Map (Словарь/Ассоциативный массив)

Map — коллекция пар ключ-значение, где ключом может быть любое значение (не только строка). Обеспечивает O(1) доступ по ключу.

Зачем нужно

Map решает задачу «найти значение по ключу» за O(1). Кэширование, подсчёт частоты, группировка, индексация — Map быстрее объекта для этих задач и поддерживает любые типы ключей.

Где используется

  • Кэширование (мемоизация)
  • Подсчёт частоты элементов
  • Хранение метаданных для объектов
  • Маршрутизация (URL → handler)
  • Локализация (ключ → перевод)
  • Конфигурации и реестры

Предпосылки

Map в JavaScript

const map = new Map();

// Установка — O(1)
map.set('name', 'Alice');
map.set(42, 'число');
map.set(true, 'булево');

const obj = { id: 1 };
map.set(obj, 'объект как ключ'); // любой тип ключа!

// Получение — O(1)
map.get('name');    // 'Alice'
map.get(42);        // 'число'
map.get(obj);       // 'объект как ключ'

// Проверка — O(1)
map.has('name');    // true

// Удаление — O(1)
map.delete('name'); // true

// Размер — O(1)
map.size;           // 3

// Очистка
map.clear();

// Создание из массива пар
const fromPairs = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

Map vs Object

Аспект Map Object
Тип ключей Любой (объект, функция, число) Строки и Symbol
Порядок Гарантирован (по вставке) Для строк — по вставке
Размер map.size (O(1)) Object.keys(obj).length (O(n))
Итерация for...of, встроенная Object.keys()/values/entries
Производительность Лучше для частых добавлений/удалений Лучше для статических данных
Прототип Нет лишних ключей Наследует от Object.prototype
JSON Не сериализуется напрямую Сериализуется
// Когда использовать Map:
// 1. Ключи — не строки
const componentState = new Map();
componentState.set(document.body, { clicks: 0 });

// 2. Частые добавления/удаления
const cache = new Map();

// 3. Нужен точный size
console.log(cache.size);

// Когда использовать Object:
// 1. JSON-сериализация
const config = { host: 'localhost', port: 3000 };
JSON.stringify(config);

// 2. Деструктуризация
const { host, port } = config;

// 3. Статичные данные с известными ключами

Итерация

const map = new Map([
  ['JS', 'JavaScript'],
  ['TS', 'TypeScript'],
  ['PY', 'Python'],
]);

// for...of (ключ-значение)
for (const [key, value] of map) {
  console.log(`${key}: ${value}`);
}

// Отдельно ключи, значения, пары
for (const key of map.keys()) { /* ... */ }
for (const value of map.values()) { /* ... */ }
for (const [k, v] of map.entries()) { /* ... */ }

// forEach
map.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

// Конвертация
const obj = Object.fromEntries(map); // Map → Object
const arr = [...map];                 // Map → Array of pairs
const newMap = new Map(Object.entries(obj)); // Object → Map

WeakMap

WeakMap хранит пары, где ключ — только объект, и не препятствует сборке мусора.

const weakMap = new WeakMap();

let element = document.querySelector('#app');
weakMap.set(element, { clicks: 0, lastVisit: new Date });

// Когда element удалён из DOM и нет других ссылок:
element = null;
// WeakMap автоматически удалит запись → нет утечки памяти

// Не поддерживает: size, итерацию, keys, values
// Используется для: приватные данные, метаданные объектов
// Паттерн: приватные данные через WeakMap
const privateData = new WeakMap();

class Person {
  constructor(name, age) {
    privateData.set(this, { name, age });
  }

  getName {
    return privateData.get(this).name;
  }

  getAge {
    return privateData.get(this).age;
  }
}

const alice = new Person('Alice', 30);
alice.getName; // 'Alice'
// Нельзя получить данные снаружи без ссылки на weakMap

Практические примеры

Подсчёт частоты

function countFrequency(array) {
  const freq = new Map();

  for (const item of array) {
    freq.set(item, (freq.get(item) || 0) + 1);
  }

  return freq;
}

const words = ['hello', 'world', 'hello', 'js', 'hello', 'world'];
console.log(countFrequency(words));
// Map { 'hello' => 3, 'world' => 2, 'js' => 1 }

Мемоизация

function memoize(fn) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveFn = memoize((n) => {
  console.log('Computing...');
  return n * n;
});

expensiveFn(5); // Computing... 25
expensiveFn(5); // 25 (из кэша, без Computing)

Two Sum (LeetCode #1)

function twoSum(nums, target) {
  const seen = new Map(); // значение → индекс

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];

    if (seen.has(complement)) {
      return [seen.get(complement), i];
    }

    seen.set(nums[i], i);
  }

  return ; // не найдено
}

console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]

Сложность операций

Операция Сложность
set(key, value) O(1)
get(key) O(1)
has(key) O(1)
delete(key) O(1)
size O(1)
Итерация O(n)

Частые ошибки

  1. Использовать Object как Map для произвольных ключей. Object автоматически конвертирует ключи в строки: obj[1] и obj["1"] — одно и то же
  2. Забывать, что Map не сериализуется в JSON. Нужна ручная конвертация через Object.fromEntries
  3. Сравнение ключей. Map использует SameValueZero (как ===, но NaN === NaN)
  4. Использовать массивы/объекты как ключи и ожидать совпадение по содержимому. map.get([1,2]) не найдёт map.set([1,2], 'x') — разные ссылки!

Практика

  1. Реализуйте groupBy(array, keyFn) используя Map
  2. Реализуйте LRU Cache (Least Recently Used) с ограниченным размером
  3. Решите задачу Two Sum (#1 LeetCode) используя Map

Связанные темы

  • Set (только уникальные значения, без пар)
  • Hash Table (Map реализован на хэш-таблице)
  • Array (упорядоченная коллекция)

Ресурсы