Menu
Русский

Map и Set в JavaScript: когда нужны, а не объекты

Разбираемся, как устроены Map и Set в JavaScript, чем они отличаются от обычных объектов и массивов и в каких случаях их реально стоит использовать.

Две коллекции сверх Object и Array

Обычные объекты и массивы покрывают большинство задач в JavaScript, но они подходят не для всего. Map и Set — это встроенные коллекции, закрывающие две конкретные ниши: поиск по ключам, когда ключ не строка, и проверка принадлежности к множеству без дубликатов.

Обе появились ещё в ES2015. Они итерируемы, у обеих есть свойство .size, и обе прекрасно работают со спред-оператором. Проще всего держать в голове такую картинку:

  • Map — как объект, но ключом может быть что угодно, а порядок элементов сохраняется.
  • Set — как массив, но значения уникальны, а проверка наличия работает быстро.

Как создать Map в JavaScript

Map хранит пары ключ/значение. Создаётся через new Map(), а для работы используются методы .set(), .get(), .has() и .delete():

index.js
Output
Click Run to see the output here.

В конструктор можно сразу передать массив пар [ключ, значение], чтобы наполнить коллекцию:

index.js
Output
Click Run to see the output here.

Такая форма «массив из двух элементов» встречается везде, где есть Map — именно так представляются записи при переборе.

Map vs Object: зачем вообще нужен Map?

Обычные объекты вроде бы делают то же самое. В большинстве случаев — да. Но Map закрывает несколько конкретных неудобных моментов:

index.js
Output
Click Run to see the output here.

Объекты наследуются от Object.prototype, поэтому у любого объекта уже есть ключи вроде toString, constructor и hasOwnProperty. У Map такого багажа нет — в нём живут только те ключи, которые вы туда положили.

Ещё несколько отличий, о которых стоит знать:

  • Ключом может быть что угодно. В Map в качестве ключа годятся объекты, функции, числа, булевы значения. А объект молча приводит любые нестроковые ключи к строкам: obj[1] и obj["1"] — это одна и та же ячейка.
  • Гарантированный порядок вставки. Map перебирается в том порядке, в котором элементы добавлялись. У объектов обычно так же, но ключи-строки, похожие на числа, сортируются первыми — неочевидная ловушка.
  • Встроенный размер. map.size работает за O(1). Для объекта пришлось бы писать Object.keys(obj).length, а это каждый раз пересоздаёт массив.
  • Заточен под частые изменения. Движки оптимизируют Map под постоянное добавление и удаление. Объекты же оптимизируются под записи со стабильной структурой.

Объект подходит, когда вы описываете запись с заранее известными строковыми ключами ({ name, email, age }). А Map стоит брать, когда ключи динамические, нестроковые, или когда записи будут часто добавляться и удаляться.

Перебор Map в JavaScript

Map — итерируемая структура, поэтому for...of работает с ним напрямую, а деструктуризация каждой пары ключ-значение получается сама собой:

index.js
Output
Click Run to see the output here.

Если нужны только ключи или только значения — вызывайте .keys() или .values(). А если вам привычнее .forEach(), он тоже есть:

index.js
Output
Click Run to see the output here.

Чтобы превратить Map обратно в обычный объект или массив, используйте spread-оператор:

index.js
Output
Click Run to see the output here.

Создание и использование Set в JavaScript

Set — это коллекция уникальных значений. Если попытаться добавить элемент, который уже есть, ничего не произойдёт:

index.js
Output
Click Run to see the output here.

Уникальность определяется по тому же правилу, что и ===, но с одной особенностью: внутри Set NaN считается равным самому себе, хотя в любом другом месте NaN === NaN даёт false.

В конструктор можно передать любой итерируемый объект — отсюда и растёт классический приём удаления дубликатов из массива в javascript:

index.js
Output
Click Run to see the output here.

Одна строка, любой примитивный тип. Для массивов объектов такой приём не сработает — два разных объекта с одинаковыми полями всё равно остаются двумя разными значениями, — но для строк, чисел и булевых это идиоматичный способ убрать дубликаты.

Set или массив: что выбрать

И массивы, и Set хранят наборы значений, так что закономерно возникает вопрос — когда что брать?

Берите Set, когда:

  • Значения должны быть уникальными, и вы хотите, чтобы рантайм сам следил за этим.
  • Часто проверяете вхождение элементов. set.has(x) работает за O(1), а array.includes(x) — за O(n). Внутри цикла эта разница очень быстро складывается в ощутимое отставание.
  • Достаточно порядка вставки. Set обходится в порядке добавления элементов, но к ним нельзя обратиться по индексу.

Оставайтесь с массивом, когда:

  • Нужен доступ по индексуarr[0], срезы, сортировка.
  • Дубликаты имеют смысл — например, корзина, в которой лежат два одинаковых товара.
  • Активно используете методы массива: .map, .filter, .reduce. У Set их нет — пришлось бы сначала разворачивать его в массив.

Небольшой пример, где разница в производительности наглядна:

index.js
Output
Click Run to see the output here.

Если бы banned был массивом, каждый вызов filter прогонял бы весь список. А с Set каждая проверка — за константное время.

Перебор Set в JavaScript

С Set всё так же, как и с Map: for...of работает из коробки, а через spread легко получить массив:

index.js
Output
Click Run to see the output here.

У Set тоже есть методы .keys(), .values() и .entries() — для симметрии с Map, хотя в случае Set ключи и значения — это одно и то же. Обычно достаточно просто пройтись по коллекции напрямую.

Практический пример: считаем уникальных посетителей по страницам

Соберём всё вместе: Map, где ключ — путь страницы, а значение — Set с ID посетителей:

index.js
Output
Click Run to see the output here.

Map отвечает за соответствие «путь → корзина», а Set — за отсутствие дубликатов внутри каждой корзины. Можно было бы обойтись обычным объектом и массивами, но тогда пришлось бы повсюду расставлять проверки через indexOf и hasOwnProperty.

Коротко про WeakMap и WeakSet

Для узкого круга задач есть две родственные коллекции: WeakMap и WeakSet. Они хранят ссылки «слабо» — то есть запись, у которой ключ (в случае WeakMap) или значение (в случае WeakSet) больше нигде не используется, автоматически собирается сборщиком мусора.

index.js
Output
Click Run to see the output here.

В качестве ключей они принимают только объекты, их нельзя перебирать, и у них нет свойства .size. Так задумано: если бы их можно было итерировать, поведение сборщика мусора стало бы наблюдаемым. Они удобны, когда нужно кэшировать метаданные об объектах, которыми вы не владеете, но в повседневном коде встречаются редко.

Дальше: JSON

Map и Set отлично работают в памяти, но ни одна из этих коллекций не переживает JSON.stringify без потерь — Map превращается в {}, Set тоже в {}. На следующей странице разберём JSON: как сериализовать и разбирать данные, а также какие приёмы применять к коллекциям из этой главы, когда им нужно пересечь границу сети или файла.

Часто задаваемые вопросы

Чем Map отличается от обычного объекта в JavaScript?

В Map ключом может быть что угодно — объект, функция, число, — а в обычном объекте ключ всегда приводится к строке (ну или к символу). Плюс у Map есть свойство .size, порядок перебора совпадает с порядком добавления, и нет прототипа — то есть ваши ключи не пересекутся случайно с toString или constructor. Берите Map, когда ключи не строки или когда часто добавляете и удаляете записи.

Зачем нужен Set в JavaScript?

Set хранит только уникальные значения — дубликаты он молча игнорирует. Самый короткий способ убрать повторы из массива — [...new Set(arr)]. Ещё у Set есть .has() со сложностью O(1), и это заметно быстрее, чем array.includes(), если вы проверяете вхождение внутри цикла.

Как перебрать Map?

Проще всего через for...of: for (const [key, value] of myMap) — здесь сразу идёт деструктуризация пары. Можно пройтись и по myMap.keys(), myMap.values() или myMap.entries(). Порядок обхода гарантированно совпадает с порядком вставки — в обычных объектах с числовыми ключами так не всегда.

Учитесь программировать с Coddy

НАЧАТЬ