Skip to main content

Set, Map, WeakSet та WeakMap у JavaScript

Set, Map, WeakSet та WeakMap - це чотири вбудовані типи колекцій у JavaScript, кожен з яких вирішує свою задачу зберігання даних.

Теорія

TL;DR

  • Set - список без дублікатів. Map - сховище пар ключ-значення, де ключами можуть бути об'єкти, а не лише рядки.
  • WeakSet і WeakMap тримають слабкі посилання (weak references) на об'єкти: якщо більше нічого не вказує на об'єкт, збирач сміття видалить запис автоматично.
  • Головний поділ: Set/Map зберігають сильні посилання і підтримують ітерацію. WeakSet/WeakMap обмінюють ітерацію на автоматичне звільнення пам'яті.
  • Set для дедуплікації, Map коли потрібні нерядкові ключі, WeakMap для метаданих DOM-вузлів або кешів.

Швидкий приклад

javascript
const set = new Set([1, 2, 2, 3]); // Set {1, 2, 3} - дублікати відкинуті const map = new Map(); map.set('name', 'Alice'); map.set({id: 1}, "object as key"); // будь-який тип підходить як ключ let node = document.querySelector('#btn'); const weakMap = new WeakMap(); weakMap.set(node, { clickCount: 0 }); // прив'язуємо дані до DOM-вузла node = null; // запис буде прибрано GC автоматично

Set прибирає дублікат 2 під час створення. Map приймає об'єкт як ключ - звичайні об'єкти так не вміють. WeakMap прив'язує дані до DOM-вузла і відпускає їх, коли вузол зникає.

Головна різниця

Set і Map тримають сильні посилання. Об'єкт, що зберігається в Map як ключ, залишається в пам'яті, поки існує сам Map, навіть якщо більше жоден код на нього не посилається. WeakSet і WeakMap використовують слабкі посилання: якщо єдиним вказівником на об'єкт залишається ключ у WeakMap, збирач сміття вважає цей об'єкт недосяжним і видаляє запис. Жодного ручного очищення.

Коли використовувати

  • Прибрати дублікати або швидко перевірити наявність елемента → Set (пошук O(1) проти O(n) у масиву)
  • Зберігати значення за нерядковими ключами, наприклад об'єктами або Symbol → Map
  • Відстежувати стан для DOM-вузлів без витоків пам'яті → WeakMap
  • Позначати вже оброблені об'єкти в алгоритмі, наприклад для визначення циклів у графах → WeakSet
  • Потрібна ітерація або .size для слабкої колекції → не вийде, використовуй Set/Map

Таблиця порівняння

ОзнакаSetMapWeakSetWeakMap
Що зберігаєЛише значенняПари ключ-значенняЛише об'єктиОб'єкти як ключі, будь-які значення
Типи ключівБудь-яке значенняБудь-яке значенняЛише об'єктиЛише об'єкти
ІтераціяТакТакНіНі
Властивість .sizeТакТакНіНі
Збирання сміттяНі (сильні посилання)Ні (сильні посилання)Так (слабкі посилання)Так (слабкі посилання)
Типовий сценарійУнікальні спискиМапи з нерядковими ключамиМітки відвіданих об'єктівКеші, метадані вузлів

Як це влаштовано всередині

V8 реалізує Set і Map як хеш-таблиці з упорядкованим зв'язним списком поверх них - саме тому порядок вставки зберігається при ітерації. Рівність перевіряється алгоритмом SameValueZero, а не ===. Відмінність: NaN === NaN у JavaScript дає false, але SameValueZero вважає два значення NaN однаковими. Тому new Set([NaN, NaN]) дасть Set {NaN} з одним елементом.

WeakSet і WeakMap використовують структуру під назвою ефемерна таблиця (ephemeron table). Ефемерон - це пара ключ-значення, де значення зберігається лише доти, поки ключ досяжний ззовні таблиці. Під час циклу збирача сміття, якщо рушій виявляє, що ключ WeakMap не має зовнішніх посилань, запис видаляється повністю. Ітерацію свідомо не підтримують: вона вимагала б від рушія тимчасово тримати сильні посилання на всі ключі, що зводить нанівець весь сенс слабких посилань.

Типові помилки

Передавати об'єктні літерали як ключі Map і очікувати збігу:

javascript
const map = new Map(); map.set({ id: 1 }, 'дані'); console.log(map.has({ id: 1 })); // false - інше посилання на об'єкт

Map порівнює ключі за посиланням (SameValueZero), а не за значенням. Два об'єкти {id: 1} - це різні об'єкти в пам'яті. Зберігай посилання в змінну, якщо потрібно потім отримати значення.

Очікувати .size або ітерацію у WeakMap чи WeakSet:

javascript
const wm = new WeakMap(); wm.set({}, 1); console.log(wm.size); // undefined for (const [k, v] of wm) {} // TypeError: wm is not iterable

.size не існує, перелічити записи теж не можна. Якщо потрібно рахувати елементи - відстежуй це окремо.

Вважати, що WeakMap автоматично запобігає всім витокам пам'яті:

javascript
let obj = { data: 'великий payload' }; const cache = new WeakMap(); cache.set(obj, 'результат обчислення'); // obj досі посилається вище - жодного GC ще не відбудеться // Лише коли ВСІ сильні посилання на obj зникнуть, запис у WeakMap прибереться

Слабкі посилання працюють лише якщо більше нічого не тримає об'єкт. Замикання, масив або звичайна змінна, що вказує на obj - і запис у WeakMap нікуди не дінеться.

Брати звичайний об'єкт замість Map для нерядкових ключів:

Звичайні об'єкти приводять всі ключі до рядків. obj[{id:1}] стає obj["[object Object]"]. Map зберігає фактичний тип, тому map.set({id:1}, value) зберігає посилання на об'єкт без жодного приведення.

Де зустрічається в реальних проектах

  • React використовує WeakMap у reconciler (Fiber), щоб прив'язувати ефекти до fiber-вузлів без блокування їх збирання.
  • Node.js AsyncHooks використовує WeakMap для відстеження async-ресурсів: завершені операції можна прибрати GC.
  • Lodash використовує Set всередині _.uniq для дедуплікації масивів за O(n).
  • Preact використовує WeakSet для відстеження хуків, що вже були зафіксовані (committed).
  • Express використовує Map для реєстрів параметрів middleware, де порядок вставки важливий.

Питання на співбесіді

Q: Що таке SameValueZero і чим відрізняється від ===?
A: SameValueZero вважає +0 і -0 рівними, і NaN рівним NaN. Звичайний === повертає false для NaN === NaN. Тому new Set([NaN, NaN]) дає Set {NaN} з одним елементом.

Q: Чому WeakSet і WeakMap не підтримують ітерацію?
A: Ітерація вимагала б від рушія перебрати всі ключі, а значить тимчасово тримати сильні посилання на них. Це блокує збирач сміття, тому ітерацію свідомо прибрали з API.

Q: Чи можуть ключі WeakMap бути примітивами - рядками або числами?
A: Ні. Примітиви - це значення, не посилання. GC відстежує посилання на об'єкти, тому лише об'єкти можуть бути слабкими ключами. Передача рядка кине TypeError.

Q: Чи є реальна різниця в продуктивності між Set і Array.includes()?
A: Set використовує хеш-таблицю, тому .has() працює за O(1) у середньому. Array.includes() сканує лінійно, O(n). Для великих колекцій або частих перевірок Set помітно швидший.

Q (рівень senior): Як реалізувати LRU-кеш за допомогою WeakMap?
A: WeakMap відповідає за прив'язку ключ-значення і автоматичне очищення, коли ключ прибирається GC. Для відстеження порядку витіснення все одно потрібен двозв'язний список або Map впорядкований за часом доступу, бо WeakMap не підтримує ітерацію. При кожному зверненні переміщуй запис на початок. При переповненні - витісняй хвіст. Якщо ключовий об'єкт прибирається GC ззовні, запис у WeakMap зникає сам, без потреби чіпати список вручну.

Приклади

Дедуплікація і перевірка наявності через Set

javascript
const visited = new Set(); function processPage(url) { if (visited.has(url)) { console.log('Вже оброблено:', url); return; } visited.add(url); console.log('Обробляємо:', url); } processPage('https://example.com'); // Обробляємо: https://example.com processPage('https://example.com'); // Вже оброблено: https://example.com processPage('https://other.com'); // Обробляємо: https://other.com console.log(visited.size); // 2

Set замінює типовий if (array.indexOf(url) !== -1) і скорочує час пошуку з O(n) до O(1). Такий самий патерн я використовував для дедуплікації навігаційної історії в SPA - перехід з масиву на Set зробив пошук по великій історії миттєвим.

Map з об'єктними ключами для контексту запитів

javascript
const requestContext = new Map(); function handleRequest(req) { requestContext.set(req, { startTime: Date.now(), userId: req.headers['x-user-id'] }); } function logRequest(req) { const ctx = requestContext.get(req); console.log(`Тривалість: ${Date.now() - ctx.startTime}мс`); requestContext.delete(req); }

Зі звичайним об'єктом req як ключ перетворився б на "[object Object]", і всі запити перемішались би. Map зберігає посилання і тримає контекст кожного запиту окремо.

WeakMap для стану DOM-вузлів без витоків пам'яті

javascript
const clickCounts = new WeakMap(); document.querySelectorAll('button').forEach(button => { clickCounts.set(button, 0); button.addEventListener('click', () => { const count = (clickCounts.get(button) || 0) + 1; clickCounts.set(button, count); console.log(`Натиснуто ${count} разів`); }); }); // Коли кнопку видаляють з DOM і більше нічого на неї не посилається, // запис у clickCounts автоматично прибирається збирачем сміття

Якби тут використовувався звичайний Map, кожна видалена кнопка залишалась би в пам'яті, поки живе сам Map. WeakMap робить очищення автоматичним.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?