Чем Map и Set отличаются от Object и Array?

MiddleJavaScript #javascript #arrays #data-structures

Вопрос

В чём разница между 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" и почему это влияет на производительность?