Чем Map и Set отличаются от Object и Array?
Вопрос
В чём разница между Map/Set и обычными Object/Array — по типам ключей, порядку перебора и производительности при частых добавлениях, удалениях и поиске?
Короткий ответ
Map хранит пары ключ-значение, где ключом может быть любой тип (включая объекты и функции), гарантирует порядок вставки и даёт O(1) для добавления/удаления/поиска. Object допускает в качестве ключей только строки и символы (числа приводятся к строкам). Set — коллекция уникальных значений любого типа с O(1) проверкой наличия, в отличие от Array.includes, который работает за O(n).
Подробный ответ
Object vs Map
- Типы ключей: у
Objectключи всегда приводятся к строке (илиSymbol) —obj[1]иobj['1']это один и тот же ключ. УMapключом может быть объект, функция,NaN, что угодно без приведения типов. - Порядок: современные движки сохраняют порядок вставки для строковых ключей объекта, но целочисленные строковые ключи (
'1','2') всегда идут первыми в числовом порядке — это неочевидное и непортируемое поведение.Mapвсегда и гарантированно сохраняет порядок вставки, независимо от типа ключа. - Размер: у
Objectнет встроенного способа узнать количество ключей (Object.keys(obj).length), уMapесть.size. - Итерация:
Objectне итерируем напрямую (for...ofне работает безObject.entries),Mapреализует протоколIterableиз коробки. - Производительность:
Mapоптимизирован под частые вставки/удаления ключей — движки используют для него отдельные хеш-структуры без побочных эффектов, связанных с тем, чтоObjectможет быть частью прототипной цепочки.Objectможет деоптимизироваться при частом добавлении/удалении свойств (переход в "dictionary mode" в V8). - Прототип: у обычного
{}есть унаследованные свойства изObject.prototype, что может привести к коллизиям ключей ('toString','__proto__').Mapне имеет этой проблемы — это чистое хранилище.
Array vs Set
- Уникальность:
Setавтоматически отбрасывает дубликаты,Array— нет, для уникальности нужно писать логику вручную. - Поиск:
Set.has()— амортизированноO(1)(хеш-таблица),Array.includes()/indexOf()—O(n), линейный проход. При большом числе проверок на вхождение разница критична. - Удаление:
Set.delete(value)—O(1), удаление элемента из массива по значению требуетindexOf+splice, то естьO(n). - Порядок: оба сохраняют порядок вставки.
- Индексация: у
Arrayесть числовые индексы и куча методов (map,filter,reduce),Setэтого не имеет — толькоadd,delete,has,forEach,size.
Когда что использовать: Object/Array — для сериализуемых данных (JSON), простых структур, где нужны привычные методы массивов. Map/Set — когда ключи не строки, важен гарантированный порядок, нужна частая проверка на существование, либо когда критична производительность при большом количестве вставок/удалений.
Пример
// Object допускает только строковые/символьные ключи
const obj = {}
const keyObj = { id: 1 }
obj[keyObj] = 'value' // keyObj приводится к строке "[object Object]"
obj['[object Object]'] = 'overwritten'
console.log(obj[keyObj]) // "overwritten" — коллизия!
// Map хранит ключи любого типа без приведения
const map = new Map()
const key1 = { id: 1 }
const key2 = { id: 2 }
map.set(key1, 'first')
map.set(key2, 'second')
console.log(map.get(key1)) // "first" — разные объекты не конфликтуют
console.log(map.size) // 2
// Порядок числовых ключей в Object — неожиданный
const weird = { b: 1, 2: 'two', a: 3, 1: 'one' }
console.log(Object.keys(weird)) // ['1', '2', 'b', 'a'] — числа впереди
// Set: уникальность и быстрый has()
const arr = [1, 2, 2, 3, 3, 3]
const unique = [...new Set(arr)]
console.log(unique) // [1, 2, 3]
const bigSet = new Set(Array.from({ length: 100000 }, (_, i) => i))
console.log(bigSet.has(99999)) // O(1) — мгновенно
const bigArr = Array.from({ length: 100000 }, (_, i) => i)
console.log(bigArr.includes(99999)) // O(n) — проход по всему массиву
// Map легко превращается обратно в объект/массив и наоборот
const entries = [['a', 1], ['b', 2]]
const fromEntries = new Map(entries)
console.log(Object.fromEntries(fromEntries)) // { a: 1, b: 2 }
Дополнительные вопросы
- Что такое
WeakMap/WeakSetи чем они полезны для управления памятью? - Почему
JSON.stringifyне умеет сериализоватьMapиSetнапрямую? - Как V8 переводит объект в "dictionary mode" и почему это влияет на производительность?