Що таке WeakMap, WeakSet, WeakRef?
WeakMap, WeakSet і WeakRef зберігають слабкі посилання (weak references) на об'єкти, тому збирач сміття (garbage collector, GC) може автоматично прибрати їх, коли на об'єкт більше нічого не вказує.
Теорія
TL;DR
- Звичайні
Map/Setутримують об'єкти в пам'яті, поки існують; слабкі версії дозволяють GC зібрати їх у будь-який момент - Аналогія: список гостей на вечірці, де ім'я зникає само собою, щойно гість іде - без жодного коду очищення
WeakMapзберігає пари "ключ-значення" (ключі тільки об'єкти);WeakSetзберігає унікальні об'єкти;WeakRefобгортає один об'єкт і дає доступ через.deref()- Жодна з трьох не підтримує ітерацію,
.sizeабо.clear()- і це задумано - Використовуй слабкі версії, коли час життя об'єкта і час життя даних не збігаються (кеші DOM-вузлів, відстеження обробників подій)
Швидкий приклад
// Map тримає obj в пам'яті навіть після obj = null
const strongMap = new Map();
let obj = { data: 'example' };
strongMap.set(obj, 'value');
obj = null; // Map досі тримає оригінальний об'єкт - пам'ять не звільнена
// WeakMap відпускає його
const weakMap = new WeakMap();
let obj2 = { data: 'example' };
weakMap.set(obj2, 'value');
obj2 = null; // GC може зібрати obj2, запис у WeakMap зникне разом із нимПісля obj2 = null сильних посилань немає. GC забирає об'єкт, і запис у WeakMap зникає автоматично.
Головна різниця
Звичайні Map і Set тримають сильні посилання. GC бачить "на цей об'єкт хтось вказує" і залишає його. Слабкі версії кажуть GC: "я знаю про цей об'єкт, але не тримай його заради мене." Якщо більше немає сильних посилань, GC збирає об'єкт і видаляє слабкий запис. Це й зупиняє витоки в кешах DOM-вузлів і реєстраторах обробників подій.
WeakMap
WeakMap - це сховище пар "ключ-значення", де ключами можуть бути тільки об'єкти. Значення - будь-що. API навмисно мінімальний: set, get, has, delete. Немає .size. Немає ітерації. Немає .keys().
Чому немає ітерації? Щоб повернути список ключів, JavaScript мав би тримати сильні посилання на всі з них. Це повністю скасувало б сенс WeakMap.
WeakSet
WeakSet зберігає унікальні об'єкти з тією ж поведінкою GC. Доступні методи: add, has, delete. Типовий сценарій - позначати об'єкти як "вже оброблені", не утримуючи їх у пам'яті.
WeakRef
WeakRef обгортає один об'єкт і дає доступ через .deref(). На відміну від WeakMap, тут немає автоматичного очищення пов'язаних даних. Він просто дає посилання, яке не блокує GC. Якщо об'єкт зібраний, .deref() поверне undefined. Перевіряй результат щоразу перед використанням.
let target = { id: 42 };
const ref = new WeakRef(target);
target = null; // через деякий час...
const obj = ref.deref();
if (obj) {
console.log(obj.id); // безпечно
} else {
console.log('вже зібрано GC');
}Коли використовувати
- Тимчасові кеші DOM-вузлів, де вузли видаляються:
WeakMap(вузол як ключ, обчислені дані як значення) - Відстеження оброблених об'єктів без утримання їх у пам'яті:
WeakSet - Опціональний кеш, де перевикористання бажане, але не обов'язкове:
WeakRefз перевіркою.deref() - Постійні дані, які мають зберігатися: звичайний
MapабоSet - Примітивні ключі (рядки, числа): звичайний
Map, бо WeakMap приймає тільки об'єкти
Таблиця порівняння
| Властивість | Map / Set | WeakMap / WeakSet / WeakRef |
|---|---|---|
| Типи ключів / значень | Будь-які (примітиви + об'єкти) | Тільки об'єкти |
| Поведінка GC | Сильне посилання, блокує GC | Слабке посилання, дозволяє GC |
| Ітерація | Так (keys, values, entries) | Ні |
.size | Так | Ні |
.clear() | Так | Ні |
| Коли використовувати | Постійне зберігання даних | Тимчасове відстеження (кеші DOM, метадані запитів) |
Як це працює в движку
V8 (Chrome і Node.js) використовує mark-sweep GC. Під час фази позначення він проходить по сильних посиланнях. Об'єкти без вхідних сильних посилань прибираються. Записи WeakMap використовують внутрішню структуру "ephemeron": запис живе, тільки якщо ключ пережив фазу позначення. WeakRef працює так само, але вимагає ручного виклику .deref(). Node.js 14+ і Chrome 84+ підтримують усі три.
Типові помилки
1. Шукати .size у WeakMap
const wm = new WeakMap();
wm.set({}, 1);
console.log(wm.size); // undefined - не помилка, просто немає такої властивостіВластивості .size немає. Якщо потрібен лічильник, ведемо його через окремий Map.
2. Використати примітив як ключ WeakMap
const wm = new WeakMap();
wm.set('key', 1); // TypeError: Invalid value used as weak map keyWeakMap приймає тільки об'єкти. Обгорни примітив в об'єкт або використовуй звичайний Map.
3. Вважати WeakRef надійним сховищем
const ref = new WeakRef({ data: 'important' });
// ... пізніше, без сильного посилання ...
ref.deref().data; // може кинути помилку - deref() може повернути undefinedЗавжди: const obj = ref.deref(); if (obj) { use(obj); }.
4. Додавати примітиви у WeakSet
const ws = new WeakSet();
ws.add(42); // TypeError: Invalid value used in weak setWeakSet для об'єктів. Для примітивів - звичайний Set.
5. Намагатися ітерувати WeakMap
const wm = new WeakMap();
for (const [k, v] of wm) { } // TypeError: wm is not iterableСлабкі колекції навмисно не підтримують ітерацію. Якщо потрібна - бери Map.
Де зустрічається в реальному коді
react-windowвикористовуєWeakMapдля кешування висот рядків з DOM-вузлами як ключами; при видаленні вузлів записи очищаються самі- Модуль
httpу Node.js використовуєWeakMapдля метаданих запитів без витоків при скасованих запитах - Lodash
_.memoizeпідтримує WeakMap-кеші для аргументів-об'єктів - Preact Signals використовують
WeakRefдля від'єднаних спостерігачів, щоб не блокувати очищення компонентів
Питання на співбесіді
Q: Що поверне let m = new WeakMap(); let o = {}; m.set(o, 1); o = null; console.log(m.has(o));?
A: false. Після o = null змінна містить null. Тому m.has(null) повертає false. Оригінальний об'єкт тепер може бути зібраний GC.
Q: Чому WeakMap не підтримує ітерацію?
A: Ітерація вимагала б сильних посилань на всі ключі одночасно. Це заблокує GC - саме те, чого WeakMap повинен уникати.
Q: Коли обрати WeakRef замість WeakMap?
A: Коли є один об'єкт і немає пов'язаного значення для зберігання. WeakMap для пар "ключ-значення", прив'язаних до часу життя об'єкта. WeakRef для перевірки "цей об'єкт ще існує?"
Q: Чи відрізняється час роботи GC у Node.js і браузері?
A: Так. Node.js V8 зазвичай агресивніший з циклами GC. Браузери обмежують GC для відгуку інтерфейсу. На практиці не покладайся на те, що .deref() поверне значення протягом конкретного часу.
Q: (Senior) Як побудувати кеш без витоків, який також відстежує кількість записів?
A: WeakMap для кешу (об'єкти звільняються автоматично) плюс звичайний Map для лічильника з ручним інкрементом при set і декрементом при відомому видаленні. Компроміс: коли GC сам видаляє записи без нашого відома, лічильник дає неточні дані. Це прийнятна поведінка для такого патерну.
Приклади
Базовий: кеш DOM-вузлів
const styleCache = new WeakMap();
function getComputedColor(node) {
if (!styleCache.has(node)) {
// getComputedStyle - дорогий DOM-виклик, кешуємо результат
styleCache.set(node, window.getComputedStyle(node).color);
}
return styleCache.get(node);
}
const button = document.querySelector('#submit');
console.log(getComputedColor(button)); // 'rgb(0, 0, 0)'
// Коли button видаляється з DOM і більше нічого на нього не вказує,
// GC збирає вузол, запис у styleCache зникає автоматично.Жодного коду очищення. Одного разу, працюючи з великим списком із рендерингом рядків, я замінив звичайний Map у калькуляторі висот на WeakMap. Використання пам'яті в Chrome DevTools впало після серії монтування та відмонтування рядків. Старий Map тихо накопичував кожен відчеплений DOM-вузол.
Середній: відстеження оброблених запитів через WeakSet
const processedRequests = new WeakSet();
async function handleRequest(req) {
if (processedRequests.has(req)) {
return; // вже оброблено
}
processedRequests.add(req);
const data = await fetch(req.url).then(r => r.json());
console.log('Оброблено:', data);
// Коли req виходить зі scope, WeakSet відпускає його автоматично.
// Зі звичайним Set кожен об'єкт запиту жив би весь час роботи сервера.
}Немає потреби викликати processedRequests.delete(req). GC займається цим сам.
Просунутий: опціональний кеш зображень через WeakRef
class ImageLoader {
#cache = new Map(); // url -> WeakRef на img-елемент
load(url) {
const existing = this.#cache.get(url);
if (existing) {
const img = existing.deref();
if (img) {
return img; // ще живий, перевикористовуємо
}
this.#cache.delete(url); // застарілий запис, прибираємо
}
const img = new Image();
img.src = url;
this.#cache.set(url, new WeakRef(img));
return img;
}
}
const loader = new ImageLoader();
const img1 = loader.load('https://example.com/photo.jpg'); // завантажує
const img2 = loader.load('https://example.com/photo.jpg'); // перевикористовує img1Коли браузеру не вистачає пам'яті, він може зібрати зображення. Перевірка .deref() обробляє обидва сценарії без падіння.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.