Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Хеш-таблицы JS: Объекты

Ассоциативный массив — абстрактный тип данных, с помощью которого хранятся пары ключ-значение. У него есть и другие названия: "словарь", "мап" (от слова map). В разных языках ему соответствуют разные типы данных. В JavaScript — это Object, в других языках:

  • Ruby — Hash
  • Lua — Table
  • Python — Dictionary
  • Elixir/Java — Map

Для чего он нужен? Ассоциативные массивы крайне популярны в прикладном программировании. С их помощью удобно представлять составные данные, содержащие множество различных параметров. Фактически все предыдущие уроки по объектам в JavaScript были посвящены тому, как использовать объекты в качестве ассоциативных массивов.

Ассоциативный массив, в отличие от обычного массива (называемого индексированным, так как значения в нем расположены по индексам), нельзя положить в память "как есть". У него нет индексов, которые бы могли определить порядок и простой способ добраться до значений. Для реализации ассоциативных массивов часто используют специальную структуру данных — хеш-таблицу. Она позволяет организовать данные ассоциативного массива удобным для хранения способом. Для этого хеш-таблица использует две вещи: индексированный массив и функцию для хеширования ключей. Обратите внимание, что хеш-таблица это не просто способ размещать данные в памяти, она включает в себя логику.

Ниже пойдет речь про то, как ассоциативные массивы бывают устроены внутри. Эта информация крайне важна для разработчиков, которые хотят по-настоящему разбираться в том, что они делают. Она снимает "магичность" с происходящего внутри языка и дает понимание цены, которую приходится платить за удобство использования объектов.

Итак, что примерно происходит, когда мы выполняем код:

const data = {};
data['key'] = 'value';

Хеширование

Любая операция внутри хеш-таблицы начинается с того, что ключ каким-то образом преобразуется в индекс обычного массива. Для получения индекса из ключа нужно выполнить два действия: найти хеш (хешировать ключ) и привести его к индексу (например, через остаток от деления).

Хеширование — операция, которая преобразует любые входные данные в строку (реже число) фиксированной длины. Функция, реализующая алгоритм преобразования, называется "хеш-функцией", а результат называют "хешем" или "хеш-суммой". Наиболее известны CRC32, MD5 и SHA (много разновидностей).

// В JavaScript нет встроенной поддержки алгоритма хеширования crc32 (удобен для наглядности)
// Поэтому используется сторонняя библиотека
import crc32 from 'crc-32';

const data = 'Hello, world!'; // Любые данные, которые мы хотим хешировать
const hash = crc32.str(data);

// Хеш всегда одинаковый для одних и тех же данных!
console.log(hash); // => -337197338

С хешированием мы встречаемся в разработке часто. Например, идентификатор коммита в git 0481e0692e2501192d67d7da506c6e70ba41e913 не что иное, как хеш, полученный в результате хеширования данных коммита.

После того, как хеш получен, его можно преобразовать в индекс массива, например, через получение остатка от деления:

// Это делается для того, чтобы индексы не были слишком большими
// Чем больше размер массива, тем больше памяти он занимает
const index = Math.abs(hash) % 1000; // по модулю
console.log(index); // => 338

хеширование

За кулисами

Рассмотрим процесс добавления нового значения в ассоциативный массив (напомню, в JavaScript представлен типом данных Object). Программист пишет:

const data = {};
data['key'] = 'value';

Такая простая, на первый взгляд, строчка, запускает целый процесс. Ниже его грубое описание, без деталей и с упрощениями:

// Для простоты показано на JavaScript, хотя в реальности всё это происходит на более низком уровне

// 1. Создание ассоциативного массива приводит к инициализации индексированного массива внутри интерпретатора.
const internal = [];
// Во время присвоения значения `data['key'] = 'value'`, интерпретатор выполняет несколько действий:

// 2. Хеширует ключ. Результатом хеширования становится число.
const hash = crc32.str('key');
// 3. Число, полученное на предыдущем шаге, преобразуется в индекс массива.
const index = Math.abs(hash) % 1000;
// В значение внутреннего индексированного массива, по найденному индексу, записывается еще один массив,
// первым элементом которого становится ключ `'key'`, а вторым значение `'value'`.
internal[index] = ['key', 'value'];

Почему такая странная структура для хранения? Зачем там нужен ключ? Ответ на этот вопрос будет ниже, там где мы поговорим про коллизии.

Теперь посмотрим на чтение:

const data = {};
data['key'] = 'value';
console.log(data['key']); // => "value"
// Для простоты показано на JavaScript, хотя в реальности всё это происходит на более низком уровне

// 1. Хешируется ключ. Результатом хеширования становится число.
const hash = crc32.str('key');
// 2. Число, полученное на предыдущем шаге преобразуется в индекс массива.
const index = Math.abs(hash % 1000);

// 3. Если индекс существует, то извлекается массив, который находился внутри, и возвращается наружу.
return internal[index]; // ['key', 'value']

Коллизии

Ключом в ассоциативном массиве может быть абсолютно любая строка (любой длины и содержания). Другими словами, множество всех возможных ключей — бесконечно. В свою очередь, результат работы хеш-функции — строка фиксированной длины, а значит множество всех выходных значений — конечно.

Из этого факта следует, что не для всех входных данных найдётся уникальный хеш. На каком-то этапе возможно появление дублей (где под одним хешем лежит несколько разных значений — как если бы под одним индексом в массиве лежало два разных элемента). Такую ситуацию принято называть коллизией. Есть несколько способов разрешения коллизий (открытая адресация, метод цепочек), и каждому из них соответствует свой тип хеш-таблицы.

// Пример коллизии
// Хеш-функция возвращает одинаковый хеш для разных строчных данных!
crc32.str('aaaaa0.462031558722291'); // 1938556049
crc32.str('aaaaa0.0585754039730588'); // 1938556049

Простейший способ разрешения коллизий, открытая адресация, предполагает последовательное перемещение по слотам хеш-таблицы в поисках первого свободного слота, куда значение будет записано. В примере выше, для второго значения будет проверен хеш 1938556050, затем, если он занят, 1938556051, и т.д. до первого незанятого хеша.

Коллизии не так редки, как может показаться. Убедиться в этом можно, изучив парадокс дней рождений.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов для веб-приложений
6 октября 10 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов для веб-приложений
6 октября 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
6 октября 16 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»