Две коллекции сверх Object и Array
Обычные объекты и массивы покрывают большинство задач в JavaScript, но они подходят не для всего. Map и Set — это встроенные коллекции, закрывающие две конкретные ниши: поиск по ключам, когда ключ не строка, и проверка принадлежности к множеству без дубликатов.
Обе появились ещё в ES2015. Они итерируемы, у обеих есть свойство .size, и обе прекрасно работают со спред-оператором. Проще всего держать в голове такую картинку:
Map— как объект, но ключом может быть что угодно, а порядок элементов сохраняется.Set— как массив, но значения уникальны, а проверка наличия работает быстро.
Как создать Map в JavaScript
Map хранит пары ключ/значение. Создаётся через new Map(), а для работы используются методы .set(), .get(), .has() и .delete():
В конструктор можно сразу передать массив пар [ключ, значение], чтобы наполнить коллекцию:
Такая форма «массив из двух элементов» встречается везде, где есть Map — именно так представляются записи при переборе.
Map vs Object: зачем вообще нужен Map?
Обычные объекты вроде бы делают то же самое. В большинстве случаев — да. Но Map закрывает несколько конкретных неудобных моментов:
Объекты наследуются от 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 работает с ним напрямую, а деструктуризация каждой пары ключ-значение получается сама собой:
Если нужны только ключи или только значения — вызывайте .keys() или .values(). А если вам привычнее .forEach(), он тоже есть:
Чтобы превратить Map обратно в обычный объект или массив, используйте spread-оператор:
Создание и использование Set в JavaScript
Set — это коллекция уникальных значений. Если попытаться добавить элемент, который уже есть, ничего не произойдёт:
Уникальность определяется по тому же правилу, что и ===, но с одной особенностью: внутри Set NaN считается равным самому себе, хотя в любом другом месте NaN === NaN даёт false.
В конструктор можно передать любой итерируемый объект — отсюда и растёт классический приём удаления дубликатов из массива в javascript:
Одна строка, любой примитивный тип. Для массивов объектов такой приём не сработает — два разных объекта с одинаковыми полями всё равно остаются двумя разными значениями, — но для строк, чисел и булевых это идиоматичный способ убрать дубликаты.
Set или массив: что выбрать
И массивы, и Set хранят наборы значений, так что закономерно возникает вопрос — когда что брать?
Берите Set, когда:
- Значения должны быть уникальными, и вы хотите, чтобы рантайм сам следил за этим.
- Часто проверяете вхождение элементов.
set.has(x)работает за O(1), аarray.includes(x)— за O(n). Внутри цикла эта разница очень быстро складывается в ощутимое отставание. - Достаточно порядка вставки.
Setобходится в порядке добавления элементов, но к ним нельзя обратиться по индексу.
Оставайтесь с массивом, когда:
- Нужен доступ по индексу —
arr[0], срезы, сортировка. - Дубликаты имеют смысл — например, корзина, в которой лежат два одинаковых товара.
- Активно используете методы массива:
.map,.filter,.reduce. УSetих нет — пришлось бы сначала разворачивать его в массив.
Небольшой пример, где разница в производительности наглядна:
Если бы banned был массивом, каждый вызов filter прогонял бы весь список. А с Set каждая проверка — за константное время.
Перебор Set в JavaScript
С Set всё так же, как и с Map: for...of работает из коробки, а через spread легко получить массив:
У Set тоже есть методы .keys(), .values() и .entries() — для симметрии с Map, хотя в случае Set ключи и значения — это одно и то же. Обычно достаточно просто пройтись по коллекции напрямую.
Практический пример: считаем уникальных посетителей по страницам
Соберём всё вместе: Map, где ключ — путь страницы, а значение — Set с ID посетителей:
Map отвечает за соответствие «путь → корзина», а Set — за отсутствие дубликатов внутри каждой корзины. Можно было бы обойтись обычным объектом и массивами, но тогда пришлось бы повсюду расставлять проверки через indexOf и hasOwnProperty.
Коротко про WeakMap и WeakSet
Для узкого круга задач есть две родственные коллекции: WeakMap и WeakSet. Они хранят ссылки «слабо» — то есть запись, у которой ключ (в случае WeakMap) или значение (в случае WeakSet) больше нигде не используется, автоматически собирается сборщиком мусора.
В качестве ключей они принимают только объекты, их нельзя перебирать, и у них нет свойства .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(). Порядок обхода гарантированно совпадает с порядком вставки — в обычных объектах с числовыми ключами так не всегда.