Map и Set в JavaScript: Исчерпывающее руководство по структурам данных

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

Введение

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

TL;DR
  • Map — словарь с ключами любого типа, сохраняющий порядок вставки. Используйте вместо объектов, когда ключи динамические, неизвестны заранее или не являются строками.

    Новое в 2024–2025: методы getOrInsert(), getOrInsertComputed() и статический Map.groupBy().

  • Set — коллекция уникальных значений. Избавляет от ручного контроля дубликатов.

    Новое в 2024–2025: встроенные операции над множествами — union(), intersection(), difference(), isSubsetOf() и другие.

  • WeakMap и WeakSet — версии со слабыми ссылками. Ключи (объекты или нерегистрируемые символы) не блокируют сборку мусора. Идеальны для приватных данных, метаинформации и кешей без утечек памяти. В отличие от обычных коллекций, не итерируемы.

  • Главный вывод: Выбор структуры данных — не вопрос вкуса, а инженерное решение. Объекты хороши для статических записей (DTO). Map и Set — для динамических коллекций. WeakMap/WeakSet — когда важна экономия памяти.

В статье — разбор каждого случая, таблица сравнения, ответы на частые вопросы и чек-лист для быстрого выбора.

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

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

Цель этой статьи — не пересказ справочной информации (она доступна в официальной документации), а формирование системного подхода к выбору структур данных. Мы рассмотрим:

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

В результате вы получите практический алгоритм, позволяющий осознанно выбирать подходящую коллекцию, опираясь не на привычку, а на понимание их внутренних свойств и области применения.

Почему Объект — плохой словарь

Прежде чем разбирать новые структуры данных, важно понять, почему вообще возникла потребность в них. В конце концов, объекты в JavaScript прекрасно работают как хранилища пар «ключ-значение». Или нет?

Многие разработчики используют объекты именно так: создают пустой объект и наполняют его динамическими свойствами. Этот подход настолько распространён, что кажется естественным. Но у него есть особенности, которые в некоторых сценариях превращаются из удобства в проблему.

Наследство, которое не просили

Когда вы создаёте Object с помощью фигурных скобок {}, он не пуст. Он наследует свойства от прототипа Object.prototype. Это означает, что в вашем «чистом» словаре уже есть несколько ключей по умолчанию: toString, hasOwnProperty, constructor и другие.

const dictionary = {};
console.log(dictionary.toString); // function toString() { [native code] } - не пусто

В большинстве случаев это не мешает — эти свойства не перечисляются в циклах и редко совпадают с реальными данными. Но когда совпадают, начинаются странные ошибки:

const userSettings = {
hasOwnProperty: 'true', // пользовательское поле с таким именем
username: 'alex'
};

// Попытка безопасной проверки существования свойства
console.log(userSettings.hasOwnProperty('username')); // TypeError: userSettings.hasOwnProperty is not a function

Возникнет ошибка: свойство hasOwnProperty в объекте перезаписало одноимённый метод прототипа. Такие баги трудно отследить, особенно если данные приходят из внешнего источника.

Можно ли это обойти? Да, создавая объект без прототипа:

const safeDictionary = Object.create(null);
safeDictionary.hasOwnProperty = 'true';
console.log(safeDictionary.hasOwnProperty); // 'true' - уже просто строка, не метод

// Но проверять существование свойства теперь нужно иначе
console.log(Object.prototype.hasOwnProperty.call(safeDictionary, 'username')); // false

Однако этот приём используется редко — о нём либо не знают, либо считают излишним. И это лишь первая проблема.

Ключи — только строки (и символы)

В объекте JavaScript ключом может быть только строка или символ. Подробнее о символах и их особенностях — в отдельной статье «Изучение Символов JavaScript». Если вы попытаетесь использовать другой тип, он будет неявно преобразован:

const object = {};
object[true] = 'boolean value';
object[42] = 'number value';
object[{ name: 'object' }] = 'object value';

console.log(object);
// { '42': 'number value', 'true': 'boolean value', '[object Object]': 'object value' }

Все ключи стали строками. Число 42 превратилось в '42', логическое значение true — в 'true', а объект — в строку '[object Object]'. Это приводит к неожиданностям:

const obj = {};
const a = { id: 1 };
const b = { id: 2 };

obj[a] = 'first';
obj[b] = 'second';

console.log(obj); // { '[object Object]': 'second' } - второй объект перезаписал первый
// Ещё один наглядный пример
const userMap = {};
const user = { id: 1 };
userMap[user] = "данные пользователя";

console.log(userMap[user]); // "данные пользователя"
console.log(userMap["[object Object]"]); // "данные пользователя" - то же значение!

// Второй объект перезаписывает первый
const anotherUser = { id: 2 };
userMap[anotherUser] = "другие данные";
console.log(userMap[user]); // "другие данные" - первое значение потеряно

Оба объекта преобразовались в одну и ту же строку, поэтому второе присваивание просто перезаписало значение первого.

В некоторых ситуациях это критично. Например, если нужно построить индекс, где ключами служат сами объекты (скажем, для реализации мемоизации или кеширования результатов функции по переданному объекту-аргументу), объект как словарь не справится, так как он не является настоящей хеш-таблицей, где ключ — это сам объект, а не его строковое представление.

Порядок ключей

Долгое время порядок ключей в объекте не был гарантирован. Сейчас ситуация изменилась: в спецификации закреплён порядок обхода собственных строковых ключей (числовые ключи идут первыми в порядке возрастания, затем строковые в порядке добавления, затем символы). Но:

  1. Это касается только собственных свойств, не унаследованных.
  2. Разные способы обхода (for...in, Object.keys, Object.getOwnPropertyNames) ведут себя по-разному.
  3. История с порядком сложна, и полагаться на неё в критическом коде всё ещё требует осторожности.

Для большинства задач порядок может быть неважен. Но когда он нужен, объект создаёт лишнюю когнитивную нагрузку: приходится помнить, как именно он будет вести себя в данной ситуации.

Размер коллекции

У объекта нет свойства size или length, которое показывало бы количество собственных свойств. Приходится использовать обходные пути:

const user = { name: 'Alex', age: 30, city: 'London' };
console.log(Object.keys(user).length); // 3

Это не катастрофа, но лишнее действие каждый раз, когда нужно узнать размер коллекции.

Отсутствие полезных методов

У объекта нет встроенных методов для типичных операций с коллекциями: проверки наличия ключа (кроме hasOwnProperty, который нужно вызывать через прототип), удаления всех элементов, обхода значений, получения итератора. Всё это приходится реализовывать поверх объекта или использовать статические методы Object.

Итог: когда объект всё ещё хорош

Сказанное не означает, что объекты — «плохие» и их не стоит использовать. Они остаются лучшим выбором для своих прямых задач:

  • Статические записи (DTO). Если структура данных известна заранее, поля фиксированы и не будут динамически добавляться — объект естественен и удобен.
  • Работа с JSON. Объекты сериализуются в JSON без дополнительных усилий.
  • Простые ассоциативные массивы со строковыми ключами. Если ключи гарантированно строки и не конфликтуют с прототипом, объект работает достаточно хорошо.

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

В следующем разделе мы рассмотрим, как Map решает эти проблемы и какие дополнительные возможности появились в последних версиях JavaScript.

Map — словарь, который вы заслужили

Если объект — это запись с фиксированными полями, то Map — это полноценный словарь для динамических коллекций. Он создавался именно для тех сценариев, где объекты проявляют свои ограничения.

Преимущества Map

Map — это коллекция пар «ключ-значение», где ключом может быть любое значение JavaScript: объект, функция, примитив, даже NaN. Порядок вставки сохраняется, и его легко обойти. Размер коллекции доступен напрямую через свойство size. И никаких неожиданных ключей из прототипа — Map всегда пуст, пока вы сами ничего в него не положили.

Сравните с объектом:

// Объект
const object = {};
console.log(object.toString); // функция - не пусто, унаследовано от прототипа

// Map
const map = new Map();
console.log(map.get('toString')); // undefined - действительно пусто

Это не просто эстетическое различие. Отсутствие прототипа означает, что данные из ненадёжных источников безопасно использовать как ключи: никакой атакующий не сможет переопределить поведение коллекции, подсунув ключ __proto__ или constructor.

Ключи любого типа

Главная особенность Map — ключи не приводятся к строкам. То, что вы используете как ключ, остаётся именно этим значением с точки зрения идентичности.

const map = new Map();
const user1 = { id: 1, name: 'Alex' };
const user2 = { id: 2, name: 'Bob' };

map.set(user1, 'Данные для Алекса');
map.set(user2, 'Данные для Боба');

console.log(map.get(user1)); // 'Данные для Алекса' - объект-ключ работает
console.log(map.get({ id: 1, name: 'Alex' })); // undefined - это другой объект

Ключи сравниваются по алгоритму SameValueZero (также известному как алгоритм сравнения значений в Map), который можно описать так: всё работает как строгое равенство (===), за одним исключением — NaN считается равным самому себе. Это обеспечивает детерминированное поведение коллекции.

const map = new Map();
map.set(NaN, 'не число');

console.log(map.get(NaN)); // 'не число' - хотя NaN !== NaN

Это делает Map предсказуемым: если вы сохранили значение по ключу, то получите его обратно, только если предъявите точно тот же ключ (ту же ссылку для объектов).

Пример кода: корзина товаров

Рассмотрим типичную задачу интернет-магазина — корзину, где товары группируются по их уникальному идентификатору, а количество может меняться.

// Товар в каталоге
class Product {
constructor(id, name, price) {
this.id = id; // число
this.name = name;
this.price = price;
}
}

// Корзина на Map
class Cart {
constructor() {
this.items = new Map(); // ключ - id товара (число), значение - { product, quantity }
}

add(product, quantity = 1) {
const id = product.id;
if (this.items.has(id)) {
// Товар уже есть - увеличиваем количество
const existing = this.items.get(id);
existing.quantity += quantity;
} else {
// Новый товар
this.items.set(id, { product, quantity });
}
}

remove(productId) {
this.items.delete(productId);
}

getTotal() {
let total = 0;
for (const { product, quantity } of this.items.values()) {
total += product.price * quantity;
}
return total;
}

showCart() {
console.log('Корзина:');
for (const [id, { product, quantity }] of this.items) {
console.log(`${product.name} x${quantity} = ${product.price * quantity} руб.`);
}
console.log(`Итого: ${this.getTotal()} руб.`);
}
}

// Использование
const cart = new Cart();
const apple = new Product(1, 'Яблоки', 50);
const milk = new Product(2, 'Молоко', 80);

cart.add(apple, 3);
cart.add(milk, 2);
cart.add(apple, 2); // добавили ещё яблок

cart.showCart();
// Корзина:
// Яблоки x5 = 250 руб.
// Молоко x2 = 160 руб.
// Итого: 410 руб.

Порядок ключей

Map запоминает порядок вставки. При обходе элементы возвращаются в той последовательности, в какой они были добавлены. Это важно для интерфейсов, где порядок отображения должен соответствовать порядку действий пользователя.

В примере с корзиной товары будут показываться в порядке добавления. Если бы мы использовали обычный объект, порядок мог бы зависеть от числовых ключей (они сортируются) или от движка JavaScript.

Новые методы

До недавнего времени типичный паттерн работы с кешем или словарём выглядел так:

if (!cache.has(key)) {
cache.set(key, computeExpensiveValue(key));
}
const value = cache.get(key);

С появлением методов getOrInsert() и getOrInsertComputed() код стал короче и выразительнее.

// getOrInsert - возвращает значение по ключу, а если ключа нет,
// вставляет указанное значение по умолчанию и возвращает его
const value = cache.getOrInsert(key, computeExpensiveValue(key));
// Внимание: computeExpensiveValue(key) выполнится всегда,
// даже если ключ уже существует!

// getOrInsertComputed - вычисляет значение по умолчанию только при необходимости
const value = cache.getOrInsertComputed(key, (key) => computeExpensiveValue(key));

Разница критична для ресурсоёмких вычислений:

const cache = new Map();

// Плохо - функция вызывается даже при попадании в кеш
const data = cache.getOrInsert(userId, fetchUserFromDatabase(userId));

// Хорошо - запрос в базу уйдёт только для новых пользователей
const data = cache.getOrInsertComputed(userId, (id) => fetchUserFromDatabase(id));

Метод принимает колбэк, который получает ключ и должен вернуть значение. Колбэк выполняется, только если ключ отсутствует.

Группировка данных: Map.groupBy()

Ещё одно полезное дополнение — статический метод Map.groupBy(). Он группирует элементы итерируемого объекта по ключам, которые возвращает предоставленная функция.

Представьте, что у нас есть массив пользователей, и мы хотим сгруппировать их по возрасту:

const users = [
{ name: 'Alex', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'Diana', age: 30 },
{ name: 'Eve', age: 35 }
];

const byAge = Map.groupBy(users, (user) => user.age); // возвращает Map, а не обычный объект

console.log(byAge);
// Map(3) {
// 25 => [ { name: 'Alex', age: 25 }, { name: 'Charlie', age: 25 } ],
// 30 => [ { name: 'Bob', age: 30 }, { name: 'Diana', age: 30 } ],
// 35 => [ { name: 'Eve', age: 35 } ]
// }

// Теперь легко получить всех пользователей определённого возраста
const age25 = byAge.get(25); // массив пользователей 25 лет

Раньше для такой группировки приходилось писать код с reduce или создавать объект вручную. Map.groupBy() делает операцию стандартной и читаемой.

Важное отличие от Object.groupBy() (который появился одновременно): Map.groupBy() сохраняет тип ключей. Если ключом выступает число, оно остаётся числом. Object.groupBy() всегда преобразует ключи в строки.

const items = [1, 2, 3, 4, 5];

const byParity = Map.groupBy(items, (n) => n % 2 === 0 ? 'even' : 'odd');
// Map(2) { 'odd' => [1, 3, 5], 'even' => [2, 4] }

const byParityObject = Object.groupBy(items, (n) => n % 2 === 0 ? 'even' : 'odd');
// { odd: [1, 3, 5], even: [2, 4] } - разницы нет, ключи и так строки

Но если ключи не строки, различие становится существенным:

const byAgeMap = Map.groupBy(users, (user) => user.age); // ключи 25, 30, 35 - числа
const byAgeObject = Object.groupBy(users, (user) => user.age);
// ключи '25', '30', '35' - строки

Производительность

Спецификация JavaScript требует, чтобы реализация Map обеспечивала доступ к элементам за время, лучшее чем линейное (сублинейное), что критично для высоконагруженных приложений.. На практике это означает сложность операций get, set, has в среднем O(1) для хеш-таблиц или O(log N) для сбалансированных деревьев.

Для объектов ситуация сложнее: движки JavaScript сильно оптимизируют объекты под сценарии «записей» с известным набором свойств. Но как только объект начинает использоваться как динамический словарь (с частым добавлением и удалением свойств), оптимизации могут отключаться, и производительность падает.

Для сценариев с частыми изменениями коллекции (добавление/удаление элементов) Map обычно выигрывает у объекта.

Границы применимости

При всех достоинствах Map не отменяет объекты полностью. Есть ситуации, где объект остаётся более естественным выбором:

  • JSON-совместимость. Map не сериализуется в JSON напрямую. Если данные нужно передавать по сети или сохранять в формате JSON, придётся преобразовывать Map в массив пар, а при получении — восстанавливать.
  • Очень маленькие коллекции. Для словаря из 2–3 фиксированных ключей объект может быть проще и читаемее.
  • Синтаксический сахар. Доступ к свойству через точку (obj.key) удобнее, чем map.get('key'), когда ключи известны заранее и являются строками.

Но для большинства задач динамического словаря — кешей, индексов, реестров, коллекций, где ключи не только строки, — Map предлагает более чистую и безопасную модель.

Когда Map может быть избыточным:

  • Для простых конфигураций со строковыми ключами. Объект с доступом через точку (config.apiKey) читается естественнее, чем config.get('apiKey').
  • Если данные нужно сериализовать в JSON: Map требует предварительного преобразования в массив пар или объект.

Set — когда важна уникальность

Если Map решает проблемы словаря, то Set закрывает другую распространённую потребность — хранение уникальных значений. До появления Set эту задачу решали массивами с ручным контролем дубликатов. Такой подход работал, но требовал лишнего кода и часто приводил к ошибкам.

Что такое Set

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

Думайте о Set как о сумке, автоматически удаляющей дубликаты. При добавлении одного и того же значения дважды сохраняется только одна копия.

const set = new Set();
set.add(1);
set.add(2);
set.add(1); // игнорируется, так как 1 уже есть

console.log(set.size); // 2
console.log(set); // Set(2) { 1, 2 }

Сравнение с массивом

Главное отличие Set от массива (Array) — автоматическое поддержание уникальности. В массиве вы можете добавить сколько угодно одинаковых элементов:

const array = [];
array.push(1);
array.push(1);
array.push(1);
console.log(array.length); // 3

В Set вторая и третья попытка добавить единицу просто ничего не изменят.

Это не означает, что Set всегда лучше массива. У каждой структуры своя область применения:

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

Проверка наличия: Set против массива

Одно из главных преимуществ Set — скорость проверки наличия элемента. Метод has() работает в среднем за O(1), тогда как Array.prototype.includes() — за O(n). Разница становится ощутимой на больших коллекциях.

const array = [];
const set = new Set();

// Заполняем обе коллекции 10000 элементами
for (let i = 0; i < 10000; i++) {
array.push(i);
set.add(i);
}

// Измеряем время проверки наличия элемента
console.time('Array includes');
array.includes(9999); // линейный поиск
console.timeEnd('Array includes');

console.time('Set has');
set.has(9999); // константное время
console.timeEnd('Set has');

На практике Set.has() может быть в десятки и сотни раз быстрее для больших коллекций (например, при работе с массивами уникальных идентификаторов или токенов). Если в коде часто встречаются проверки вида if (array.includes(item)) и массив может расти, стоит рассмотреть замену на Set для оптимизации производительности и алгоритмической сложности.

Удаление дубликатов из массива

Простейшее и самое известное применение Set — фильтрация уникальных значений:

const numbers = [1, 2, 3, 2, 4, 1, 5, 3];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]

Этот паттерн настолько распространён, что стал идиоматическим способом получения уникальных значений в JavaScript.

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

Представим задачу: нужно отслеживать уникальных посетителей сайта за день. Каждый визит регистрируется, но одного пользователя не следует учитывать дважды.

class VisitorTracker {
constructor() {
this.visitors = new Set(); // уникальные идентификаторы посетителей
this.totalVisits = 0; // общее число визитов (с учётом повторных)
}

// Регистрация визита
registerVisit(userId) {
this.totalVisits++;
this.visitors.add(userId); // Set сам позаботится об уникальности
}

// Количество уникальных посетителей
getUniqueCount() {
return this.visitors.size;
}

// Процент новых посетителей (уникальных от общего числа)
getNewVisitorPercentage() {
if (this.totalVisits === 0) return 0;
return (this.visitors.size / this.totalVisits) * 100;
}

// Был ли пользователь на сайте (когда-либо)
hasVisited(userId) {
return this.visitors.has(userId);
}
}

// Использование
const tracker = new VisitorTracker();

// Имитация визитов
tracker.registerVisit('user_1');
tracker.registerVisit('user_2');
tracker.registerVisit('user_1'); // повторный визит
tracker.registerVisit('user_3');
tracker.registerVisit('user_2'); // ещё один повторный

console.log(`Уникальных посетителей: ${tracker.getUniqueCount()}`); // 3
console.log(`Всего визитов: ${tracker.totalVisits}`); // 5
console.log(`Процент новых: ${tracker.getNewVisitorPercentage()}%`); // 60%

Если бы мы использовали массив, пришлось бы каждый раз проверять if (!visitors.includes(userId)) visitors.push(userId), что медленнее и требует лишней операции.

Операции над множествами

Долгое время главным недостатком Set было отсутствие встроенных операций над множествами. Разработчикам приходилось писать собственные функции для объединения, пересечения и разности. С 2024 года ситуация изменилась: в язык добавлены методы, реализующие стандартные операции теории множеств.

Рассмотрим их на примере двух наборов интересов пользователей:

const aliceInterests = new Set(['music', 'hiking', 'cooking', 'reading']);
const bobInterests = new Set(['hiking', 'gaming', 'reading', 'photography']);

Объединение (union) — элементы, которые есть хотя бы в одном множестве:

const either = aliceInterests.union(bobInterests);
console.log([...either]);
// ['music', 'hiking', 'cooking', 'reading', 'gaming', 'photography']

Пересечение (intersection) — элементы, общие для обоих множеств:

const both = aliceInterests.intersection(bobInterests);
console.log([...both]); // ['hiking', 'reading']

Разность (difference) — элементы первого множества, которых нет во втором:

const onlyAlice = aliceInterests.difference(bobInterests);
console.log([...onlyAlice]); // ['music', 'cooking']

const onlyBob = bobInterests.difference(aliceInterests);
console.log([...onlyBob]); // ['gaming', 'photography']

Симметрическая разность (symmetricDifference) — элементы, которые есть только в одном из множеств (объединение минус пересечение):

const unique = aliceInterests.symmetricDifference(bobInterests);
console.log([...unique]); // ['music', 'cooking', 'gaming', 'photography']

Проверка отношений множеств:

const mammals = new Set(['cat', 'dog', 'whale', 'human']);
const pets = new Set(['cat', 'dog']);

console.log(pets.isSubsetOf(mammals)); // true - все питомцы являются млекопитающими
console.log(mammals.isSupersetOf(pets)); // true - млекопитающие включают всех питомцев
console.log(pets.isDisjointFrom(new Set(['bird', 'fish']))); // true - нет общих элементов

Эти методы делают Set полноценным инструментом для работы с множествами в математическом смысле.

Взаимодействие с другими структурами

Важная особенность новых методов: они работают не только с Set, но с любыми объектами, реализующими «set-like» протокол. Такой объект должен иметь:

  • свойство size
  • метод has()
  • метод keys() (возвращающий итератор элементов)

Интересно, что Map подходит под это описание — его keys() возвращает итератор ключей. Поэтому можно делать, например, пересечение Set и ключей Map:

const userMap = new Map([
['alice', { age: 25 }],
['bob', { age: 30 }],
['charlie', { age: 35 }]
]);

const activeUsers = new Set(['alice', 'charlie', 'diana']);

// Пользователи, которые есть и в Map, и в Set активных
const activeInMap = activeUsers.intersection(userMap);
console.log([...activeInMap]); // ['alice', 'charlie']

Массивы, напротив, не являются set-like — у них нет метода has(), а keys() возвращает индексы, а не значения.

Границы применимости

Set не заменяет массивы, но дополняет их. Вот несколько ориентиров:

Используйте Set, когда:

  • Нужно гарантировать уникальность значений
  • Критична скорость проверки наличия элемента (has())
  • Требуются математические операции над множествами
  • Данные не имеют внутреннего порядка, кроме порядка добавления
  • Нужно быстро удалять дубликаты из массива

Когда Set может быть не лучшим выбором:

  • Если нужны методы массива — map, filter, reduce. Set не имеет встроенных методов для трансформации данных, их придётся преобразовывать в массив.
  • Если нужен доступ по индексу или важен порядок элементов, отличный от порядка вставки.
  • Если данные нужно сериализовать в JSON: JSON.stringify(new Set()) вернёт пустой массив [], а не представление данных.

Используйте массив, когда:

  • Важен порядок элементов (например, сортированный список)
  • Нужен доступ по индексу
  • Дубликаты допустимы и информативны
  • Требуется работа с методами массива (map, filter, reduce в цепочках)

Часто эффективна комбинация: Set для проверки уникальности и быстрого доступа, а массив — для отображения в нужном порядке.

Совместное использование Set и Map

Set и Map часто эффективно работают в паре. Рассмотрим пример менеджера пользовательских сессий, где Set отслеживает уникальных активных пользователей, а Map хранит подробные данные сеанса:

class SessionManager {
constructor() {
this.activeSessions = new Map(); // userId -> данные сессии
this.loggedInUsers = new Set(); // уникальные ID пользователей
}

login(userId, sessionData) {
this.loggedInUsers.add(userId); // O(1) - добавление
this.activeSessions.set(userId, {
data: sessionData,
startTime: Date.now()
});
}

isLoggedIn(userId) {
return this.loggedInUsers.has(userId); // O(1) - проверка
}

getSessionData(userId) {
return this.activeSessions.get(userId); // O(1) - поиск
}
}

Такая комбинация эффективна, поскольку разделяет задачи: Set отвечает за уникальность и быструю проверку наличия, а Map — за хранение дополнительных данных, связанных с каждым пользователем. Мгновенное время поиска (O(1)) делает этот подход идеальным для приложений с высокой нагрузкой.

WeakMap и WeakSet

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

Утечки памяти

Представьте, что вы пишете приложение, которое обрабатывает DOM-элементы. Вам нужно сохранить для каждого элемента дополнительную информацию — например, состояние тултипа или обработчики событий.

// Плохой пример с Map
const elementData = new Map();

function processElement(element) {
elementData.set(element, {
processed: true,
timestamp: Date.now()
});
// ... какая-то работа с элементом
}

// Где-то в коде элемент удаляется из DOM
// element.remove();
// Но ссылка на него всё ещё живёт в elementData!
// Сборщик мусора не может освободить память

Элемент удалён из документа, на него нет ссылок в коде... кроме одной — он остаётся ключом в Map. Пока запись существует в Map, объект не будет собран сборщиком мусора (GC). Это классическая утечка памяти, которая нарушает управление памятью и удлиняет жизненный цикл объекта за пределы необходимого.

Как работают WeakMap и WeakSet

WeakMap и WeakSet — это версии Map и Set со слабыми ссылками на ключи (в WeakMap) и значения (в WeakSet). Слабая ссылка не препятствует сборке мусора. Если на объект нет других ссылок, кроме слабых, он может быть удалён, и запись автоматически исчезнет из коллекции.

const weakMap = new WeakMap();
let element = document.getElementById('temp');

weakMap.set(element, { info: 'временные данные' });
console.log(weakMap.has(element)); // true

// Удаляем ссылку на элемент
element.remove();
element = null;

// Через некоторое время сборщик мусора удалит объект
// weakMap больше не содержит эту запись

Ключи и значения

WeakMap принимает ключами только объекты и нерегистрируемые символы. Примитивы (строки, числа) не могут быть ключами. Значения могут быть любыми.

const wm = new WeakMap();
const obj = { id: 1 };

wm.set(obj, 'некоторая информация');
wm.set(Symbol('уникальный'), 'значение'); // символ (незарегистрированный) тоже можно

// Это не сработает:
// wm.set('string', 'value'); // TypeError: Invalid value used as weak map key

О различиях между обычными и зарегистрированными символами, а также о том, почему первые можно использовать в WeakMap, рассказываем в статье «Изучение Символов JavaScript».

WeakSet хранит только объекты и нерегистрируемые символы. Добавить примитив нельзя.

const ws = new WeakSet();
const obj1 = { id: 1 };
const obj2 = { id: 2 };

ws.add(obj1);
ws.add(obj2);
ws.add(Symbol('метка')); // символ можно

console.log(ws.has(obj1)); // true

// Это не сработает:
// ws.add(42); // TypeError: Invalid value used in weak set

Почему их нельзя итерировать

Отсутствие методов обхода (keys(), values(), entries(), forEach) и свойства size — не недостаток, а сознательное проектное решение. Если бы можно было получить список ключей WeakMap, этот список зависел бы от состояния сборщика мусора. Один и тот же код в разное время мог бы возвращать разное количество элементов, что привело бы к недетерминированному поведению.

JavaScript спроектирован так, чтобы сборка мусора была невидима для программиста. Вы не можете точно знать, когда объект будет удалён, и не должны иметь возможность наблюдать это через API коллекций. Поэтому WeakMap и WeakSet предоставляют только минимальный интерфейс: set/add, get (только для WeakMap), has, delete.

Приватные данные через WeakMap

Одно из классических применений WeakMap — создание приватных полей, недоступных извне. Рассмотрим пример с классом Person, где приватные данные хранятся в 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;
}

celebrateBirthday() {
const data = privateData.get(this);
data.age += 1;
console.log(`${data.name} теперь ${data.age} лет!`);
}
}

const alice = new Person('Алиса', 30);
console.log(alice.getName()); // 'Алиса'
console.log(alice.age); // undefined - напрямую не достучаться

// Даже если получить ссылку на внутренний объект, приватные данные защищены
console.log(privateData.get(alice)); // { name: 'Алиса', age: 30 } - но это уже намеренный доступ

Преимущества этого подхода:

  • Приватные данные изолированы от публичного API
  • Методы класса имеют к ним доступ
  • Когда экземпляр класса уничтожается, связанные с ним данные автоматически удаляются из WeakMap

Современный JavaScript предлагает нативные приватные поля (синтаксис #name), но WeakMap остаётся полезным инструментом для более сложных сценариев, где приватность должна сочетаться с дополнительной логикой.

Метаданные для DOM-элементов

Вернёмся к примеру с DOM-элементами. Используя WeakMap, мы решаем проблему утечек памяти:

const tooltipState = new WeakMap();

function setupTooltip(element, text) {
// Сохраняем состояние тултипа
tooltipState.set(element, {
text,
visible: false,
createdAt: Date.now()
});

element.addEventListener('mouseenter', () => {
const state = tooltipState.get(element);
if (state) {
showTooltip(element, state.text);
state.visible = true;
}
});

element.addEventListener('mouseleave', () => {
const state = tooltipState.get(element);
if (state) {
hideTooltip(element);
state.visible = false;
}
});
}

function updateTooltipText(element, newText) {
const state = tooltipState.get(element);
if (state) {
state.text = newText;
}
}

// Использование
const button = document.getElementById('myButton');
setupTooltip(button, 'Нажми меня!');

// Если кнопка будет удалена из DOM и все ссылки на неё исчезнут,
// запись в tooltipState автоматически удалится сборщиком мусора

Это надёжнее, чем добавлять свойства прямо в DOM-элемент (риск конфликтов имён) или использовать Map (риск утечек).

Кеширование результатов функций

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

const cache = new WeakMap();

function processExpensiveData(data) {
if (cache.has(data)) {
console.log('Возвращаем кешированный результат');
return cache.get(data);
}

console.log('Вычисляем результат...');
// Имитация дорогой обработки
const result = {
processed: true,
timestamp: Date.now(),
inputSize: JSON.stringify(data).length
};

cache.set(data, result);
return result;
}

// Использование
let input = { userId: 42, data: [1, 2, 3, 4, 5] };
const res1 = processExpensiveData(input); // вычисляем
const res2 = processExpensiveData(input); // из кеша

// Когда input больше не нужен и ссылка на него исчезает,
// запись в кеше тоже может быть удалена
input = null;
// Сборщик мусора рано или поздно освободит память

WeakSet для отслеживания факта обработки

WeakSet удобен, когда нужно просто пометить объекты как «обработанные» или «посещённые», не храня дополнительных данных:

const processed = new WeakSet();

function processIfNeeded(obj) {
if (processed.has(obj)) {
console.log('Объект уже обработан, пропускаем');
return;
}

console.log('Обрабатываем объект...');
// ... какая-то обработка

processed.add(obj);
}

// Использование
let data = { id: 1, value: 'test' };
processIfNeeded(data); // обрабатываем
processIfNeeded(data); // пропускаем

// Когда data больше не нужна, она исчезнет и из processed

Это эффективнее, чем добавлять флаг в сам объект (например, data.processed = true), и безопаснее, чем хранить обработанные объекты в Set, который заблокирует их удаление.

Ограничения и когда использовать

WeakMap и WeakSet — инструменты для специфических ситуаций. Они не заменяют обычные коллекции, а дополняют их.

Используйте WeakMap/WeakSet, когда:

  • Нужно связать дополнительные данные с объектом, не вмешиваясь в его структуру
  • Время жизни данных должно совпадать с временем жизни объекта
  • Вы не можете контролировать, когда объект будет удалён (например, DOM-элементы)
  • Важно избежать утечек памяти при долгоживущих коллекциях
  • Нужны приватные поля, защищённые от случайного доступа извне

Не используйте WeakMap/WeakSet, когда:

  • Нужно перебрать все элементы коллекции (они не итерируемы)
  • Нужно получить размер коллекции (нет свойства size)
  • Ключами должны быть примитивы
  • Данные должны сохраняться независимо от времени жизни объекта

Сравнение и таблица принятия решений

После детального разбора каждой структуры данных возникает закономерный вопрос: какую из них выбрать в конкретной ситуации? В этой части мы сведём всё воедино и предложим практический инструмент для принятия решений.

Критерии выбора

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

  1. Тип ключей — будут ли они только строками или могут быть любого типа?
  2. Динамика — известен ли набор ключей заранее или он будет меняться?
  3. Уникальность — нужно ли гарантировать уникальность значений?
  4. Порядок — важен ли порядок элементов и должен ли он сохраняться?
  5. Производительность — как часто будут операции добавления, удаления, поиска?
  6. Память — нужно ли избегать утечек при работе с временными объектами?
  7. Сериализация — потребуется ли передавать данные в JSON или другие форматы?

Таблица сравнения

КритерийObjectMapArraySetWeakMapWeakSet
Типы ключейСтроки, символыЛюбыеЧисловые индексыНе применимоОбъекты, символы *Не применимо
Типы значенийЛюбыеЛюбыеЛюбыеЛюбыеЛюбыеОбъекты, символы *
Уникальность ключейДаДаНет (индексы)Да
Уникальность значенийНетНетНетДаНетДа
Порядок обходаСложный, зависит от методаПорядок вставкиПорядок индексовПорядок вставкиНе итерируемНе итерируем
Размер (количество)Object.keys(obj).lengthmap.sizearray.lengthset.sizeНетНет
Проверка наличияkey in obj / obj.hasOwnProperty(key)map.has(key)array.includes(value)set.has(value)weakMap.has(key)weakSet.has(value)
Производительность (частое добавление / удаление)Средняя (может деградировать)ВысокаяНизкая (при вставке/удалении)ВысокаяВысокаяВысокая
Управление памятьюСильные ссылкиСильные ссылкиСильные ссылкиСильные ссылкиСлабые ссылки на ключиСлабые ссылки на значения
Сериализация в JSONПрямаяТребует преобразованияПрямаяТребует преобразованияНетНет
Доступ к элементуobj.key или obj[key]map.get(key)array[index]Через итератор или forEachweakMap.get(key)Только проверка наличия

* Нерегистрируемые символы

Алгоритм выбора

Используйте этот чек-лист, когда сомневаетесь:

  1. Нужна ли сериализация в JSON прямо сейчас, без дополнительных преобразований?
    • Да → Объект или Массив
    • Нет → идём дальше
  2. Ключи (если они есть) могут быть не строками?
    • Да → Map (или WeakMap для временных объектов)
  3. Нужно гарантировать уникальность значений?
    • Да → Set (или WeakSet для временных объектов)
  4. Данные временные, связаны с объектами и не должны влиять на сборку мусора?
    • Да → WeakMap или WeakSet
  5. Нужен доступ по индексу, сортировка, дубликаты допустимы?
    • Да → Массив
  6. Всё остальное (ключи-строки, фиксированная структура)
    • Объект
Блок-схема выбора структуры данных в JavaScript
Алгоритм выбора: Map, Set, Object, Array, WeakMap, WeakSet

Частые ошибки при выборе

  1. Использование объекта как словаря по умолчанию. Если ключи динамические, вы рискуете натолкнуться на конфликт с прототипом или проблемы с приведением типов.
  2. Использование массива вместо Set для уникальных значений. Код с array.includes(value) перед добавлением работает медленнее и требует лишних действий.
  3. Хранение временных данных в Map. Если ключи — объекты с ограниченным временем жизни, Map не даст сборщику мусора их удалить.
  4. Попытка итерировать WeakMap. Это невозможно по дизайну, и попытки обойти ограничение говорят о том, что выбрана не та структура.
  5. Сериализация Map в JSON без преобразования. Map просто превратится в пустой объект. Всегда преобразуйте в массив пар перед отправкой.
  6. Использование Set и Map там, где достаточно простых структур. Для небольших коллекций (2–3 элемента) или статических конфигураций со строковыми ключами объекты и массивы могут быть проще, читаемее и даже быстрее за счёт оптимизаций движка JavaScript.

Рекомендации

В современном JavaScript нет универсальной структуры данных «на все случаи жизни». Каждая коллекция занимает свою нишу:

  • Объекты — для статических записей и данных, которые будут сериализоваться в JSON.
  • Map — для динамических словарей, особенно с нестроковыми ключами и частыми изменениями.
  • Set — для коллекций уникальных значений и операций над множествами.
  • Массивы — для упорядоченных списков, где важны индекс, сортировка и дубликаты.
  • WeakMap и WeakSet — для метаданных, приватных данных и кешей, которые не должны мешать сборке мусора.

Выбор правильной структуры — не вопрос вкуса, а инженерное решение, влияющее на производительность, надёжность и поддерживаемость кода. Вооружившись пониманием их свойств, вы сможете делать этот выбор осознанно.

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

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

Почему Map не сериализуется в JSON автоматически? Это баг?

Нет, это не баг, а следствие дизайна. JSON был разработан для работы с простыми структурами данных: объектами (со строковыми ключами), массивами, строками, числами, булевыми значениями и null. Map же допускает ключи любого типа, включая объекты и функции, которые не имеют представления в JSON.

Если бы JSON.stringify() пытался сериализовать Map, возникал бы вопрос: что делать с ключом-объектом? Превращать его в строку [object Object]? Это привело бы к потере информации и неожиданному поведению.

Что делать? Преобразовывать Map в массив пар перед сериализацией:

const map = new Map([['name', 'freeCodeCamp'], ['age', 10], [true, 'boolean']]);

// При сериализации
const asJSON = JSON.stringify([...map]);
// Результат: '[[ "name", "freeCodeCamp"], ["age", 10], [true, "boolean"]]'

// При десериализации
const mapBack = new Map(JSON.parse(asJSON));

Если ключи гарантированно строки, можно преобразовать в объект:

const map = new Map([['name', 'freeCodeCamp'], ['age', 10]]);
const asObject = Object.fromEntries(map);
const asJSON = JSON.stringify(asObject); // '{"name":"freeCodeCamp","age":10}'
Можно ли использовать NaN как ключ в Map?

Да, и это работает предсказуемо. Несмотря на то, что в JavaScript NaN !== NaN, в MapSet) используется алгоритм сравнения SameValueZero, который считает NaN равным самому себе.

const map = new Map();
map.set(NaN, 'не число');

console.log(map.get(NaN)); // 'не число' - работает!
console.log(map.get(Number('abc'))); // тоже 'не число', потому что Number('abc') даёт NaN

Это удобно, когда нужно использовать результат математических операций, который может оказаться NaN, в качестве ключа.

Что произойдёт, если добавить в Set объект, а потом изменить этот объект?

Объект останется в Set, потому что идентичность объекта не меняется. Set определяет уникальность по ссылке на объект, а не по его содержимому.

const set = new Set();
const obj = { name: 'Алекс' };

set.add(obj);
console.log(set.has(obj)); // true

// Изменяем объект
obj.name = 'Александр';
console.log(set.has(obj)); // true - это тот же объект

// Но новый объект с тем же содержимым - это другой объект
const anotherObj = { name: 'Александр' };
console.log(set.has(anotherObj)); // false
Как быстро очистить Map или Set? Нужно ли создавать новый?

Используйте метод clear(). Он удаляет все элементы за O(1) или O(n) в зависимости от реализации, но в любом случае это эффективнее, чем создавать новую коллекцию.

const map = new Map([[1, 'one'], [2, 'two']]);
map.clear();
console.log(map.size); // 0 - коллекция пуста, но объект тот же

// Против создания нового
map = new Map(); // работает, но старая коллекция остаётся в памяти до сборки мусора

Метод clear() удобен, когда на коллекцию есть другие ссылки, и вы хотите очистить именно её, не заменяя объект.

Почему WeakMap не позволяет итерировать ключи?

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

JavaScript спроектирован так, чтобы сборка мусора была невидима для программиста. Поэтому WeakMap и WeakSet предоставляют только минимальный интерфейс: проверку наличия, добавление, удаление и (для WeakMap) получение значения. Никаких методов обхода или свойства size.

Когда действительно стоит использовать WeakMap для приватных данных, если есть нативные приватные поля?

Нативные приватные поля (синтаксис #) предпочтительнее в большинстве случаев. Они читаются, поддерживаются всеми современными средами и дают настоящую приватность.

Однако WeakMap остаётся полезным в нескольких сценариях:

  1. Когда нужно добавить приватные данные к объектам, которые вы не контролируете (например, к DOM-элементам или объектам из сторонних библиотек).
  2. Когда нужно, чтобы разные части кода имели доступ к разным наборам приватных данных, ассоциированных с одним объектом. Каждая WeakMap может хранить свой срез данных.
  3. Для обратной совместимости, если нужно поддерживать окружения без поддержки приватных полей.
// Ситуация: добавляем метаданные к DOM-элементам
const elementMetadata = new WeakMap();

function trackClick(element) {
if (!elementMetadata.has(element)) {
elementMetadata.set(element, { clickCount: 0 });
}

const data = elementMetadata.get(element);
data.clickCount++;

console.log(`Элемент кликнут ${data.clickCount} раз`);
}
Можно ли использовать символы как ключи в WeakMap?

Да, но только нерегистрируемые символы. Символы, созданные вызовом Symbol(), могут использоваться как ключи в WeakMap. Они ведут себя как объекты с точки зрения сборки мусора: если на символ нет других ссылок, он может быть удалён.

const wm = new WeakMap();
const sym = Symbol('уникальный');

wm.set(sym, 'значение для символа');
console.log(wm.get(sym)); // 'значение для символа'

// Если потом сделать sym = null, запись может быть удалена сборщиком мусора

Зарегистрированные символы (Symbol.for('key')) так использовать нельзя — они глобальны и не могут быть собраны сборщиком мусора.

Что быстрее: Object или Map для небольшого количества ключей?

Для очень маленьких коллекций (2–3 ключа) объект может быть быстрее. Современные движки JavaScript сильно оптимизируют объекты, особенно если структура свойств предсказуема. Объекты, созданные с одним и тем же набором ключей, могут разделять скрытые классы, что даёт очень быстрый доступ к свойствам.

Однако для большинства реальных сценариев разница незаметна. Выбирайте структуру исходя из семантики задачи, а не из микрооптимизаций. Если позже выяснится, что именно этот участок кода критичен по производительности, можно будет измерить и принять решение на основе данных.

Как преобразовать Set в массив и обратно?

SetArray:

const set = new Set([1, 2, 3, 4]);

// Способ 1: spread-оператор
const arr1 = [...set];

// Способ 2: Array.from
const arr2 = Array.from(set);

ArraySet (с удалением дубликатов):

const arr = [1, 2, 3, 2, 1, 4];
const set = new Set(arr); // Set(4) {1, 2, 3, 4}
Почему у Set есть методы keys() и entries(), если ключей нет?

Для совместимости с Map. Метод keys() возвращает то же самое, что и values() — итератор значений. Метод entries() возвращает итератор, где каждый элемент представлен как [value, value]. Это сделано для того, чтобы можно было использовать одни и те же паттерны обхода для Map и Set.

const set = new Set(['a', 'b', 'c']);

console.log([...set.keys()]); // ['a', 'b', 'c']
console.log([...set.values()]); // ['a', 'b', 'c']
console.log([...set.entries()]); // [['a', 'a'], ['b', 'b'], ['c', 'c']]

Если вы пишете код, который должен работать и с Map, и с Set, вы можете рассчитывать на наличие этих методов.

Можно ли сделать WeakMap итерируемым, если очень нужно?

Нет, и не пытайтесь. Это ограничение на уровне спецификации языка, и его нельзя обойти легальными средствами. Если вам нужна итерируемая коллекция со слабыми ссылками, возможно, вы неправильно оценили задачу.

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

Что такое «нерегистрируемые символы» и почему они важны для WeakMap и WeakSet?

Нерегистрируемые символы — это символы, созданные через Symbol() без Symbol.for(). Они уникальны и не попадают в глобальный реестр символов. Их важность в том, что они, как и объекты, могут быть собраны сборщиком мусора, если на них нет ссылок.

// Нерегистрируемый символ - может использоваться в WeakMap
const localSym = Symbol('local');

// Зарегистрированный символ - НЕ может использоваться в WeakMap
const globalSym = Symbol.for('global');

Это расширяет возможности WeakMap и WeakSet: теперь они могут работать не только с объектами, но и с символами, которые тоже подлежат сборке мусора.

Заключение

Объекты — для статических записей (DTO) и данных, которые сериализуются в JSON. Не подходят как динамические словари из-за наследования от прототипа, приведения ключей к строкам и отсутствия гарантий порядка.

Map — для динамических словарей с ключами любого типа, частыми изменениями и необходимостью сохранять порядок вставки. Новые методы getOrInsert(), getOrInsertComputed() и Map.groupBy() упрощают работу с отсутствующими ключами и группировкой данных.

Set — для коллекций уникальных значений и операций над множествами (union, intersection, difference, isSubsetOf). Быстрее массива при проверке наличия элемента и не требует ручного контроля дубликатов.

WeakMap и WeakSet — для метаданных, приватных данных и кешей, связанных с объектами. Слабые ссылки не препятствуют сборке мусора, что исключает утечки памяти. Неитерируемые — это не баг, а защита от недетерминизма.

Чек-лист для быстрого выбора

  1. Статическая запись с известными полями?Объект
  2. Упорядоченный список с возможными дубликатами?Массив
  3. Ключи динамические, не только строки?Map
  4. Ключи — временные объекты, нужна защита от утечек?WeakMap
  5. Нужна гарантия уникальности значений?Set
  6. Значения — временные объекты, нужна защита от утечек?WeakSet
  7. Нужны операции над множествами (пересечение, объединение)?Set
  8. Нужна группировка данных по динамическим ключам?Map.groupBy()
  9. JSON сейчас и без преобразований?Объект или Массив

Выбор структуры данных в JavaScript — это не вопрос привычки, а инженерное решение, от которого напрямую зависит надёжность, скорость и масштабируемость вашего кода. Следование best practices в этом вопросе напрямую влияет на поддерживаемость кодовой базы.


Дополнительные материалы

Предыдущая Статья

JavaScript: Более безопасное чтение и запись URL

Следующая Статья

PHP: Абстрактная Фабрика