Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке WeakMap, WeakSet, WeakRef?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**WeakMap, WeakSet і WeakRef** зберігають слабкі посилання на об'єкти, дозволяючи GC зібрати їх автоматично, коли сильних посилань більше немає. На відміну від `Map` і `Set`, слабкі версії не блокують збирач сміття. ```javascript const cache = new WeakMap(); let node = document.querySelector('#btn'); cache.set(node, { color: 'red' }); node = null; // node може бути зібраний GC; запис у кеші зникне разом із ним ``` **Ключове:** слабкі посилання відстежують об'єкти, але не утримують їх.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**WeakMap, WeakSet і WeakRef** зберігають слабкі посилання (weak references) на об'єкти, тому збирач сміття (garbage collector, GC) може автоматично прибрати їх, коли на об'єкт більше нічого не вказує. ## Теорія ### TL;DR - Звичайні `Map`/`Set` утримують об'єкти в пам'яті, поки існують; слабкі версії дозволяють GC зібрати їх у будь-який момент - Аналогія: список гостей на вечірці, де ім'я зникає само собою, щойно гість іде - без жодного коду очищення - `WeakMap` зберігає пари "ключ-значення" (ключі тільки об'єкти); `WeakSet` зберігає унікальні об'єкти; `WeakRef` обгортає один об'єкт і дає доступ через `.deref()` - Жодна з трьох не підтримує ітерацію, `.size` або `.clear()` - і це задумано - Використовуй слабкі версії, коли час життя об'єкта і час життя даних не збігаються (кеші DOM-вузлів, відстеження обробників подій) ### Швидкий приклад ```javascript // 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`. Перевіряй результат щоразу перед використанням. ```javascript 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** ```javascript const wm = new WeakMap(); wm.set({}, 1); console.log(wm.size); // undefined - не помилка, просто немає такої властивості ``` Властивості `.size` немає. Якщо потрібен лічильник, ведемо його через окремий `Map`. **2. Використати примітив як ключ WeakMap** ```javascript const wm = new WeakMap(); wm.set('key', 1); // TypeError: Invalid value used as weak map key ``` WeakMap приймає тільки об'єкти. Обгорни примітив в об'єкт або використовуй звичайний `Map`. **3. Вважати WeakRef надійним сховищем** ```javascript const ref = new WeakRef({ data: 'important' }); // ... пізніше, без сильного посилання ... ref.deref().data; // може кинути помилку - deref() може повернути undefined ``` Завжди: `const obj = ref.deref(); if (obj) { use(obj); }`. **4. Додавати примітиви у WeakSet** ```javascript const ws = new WeakSet(); ws.add(42); // TypeError: Invalid value used in weak set ``` WeakSet для об'єктів. Для примітивів - звичайний `Set`. **5. Намагатися ітерувати WeakMap** ```javascript 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-вузлів ```javascript 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 ```javascript 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 ```javascript 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()` обробляє обидва сценарії без падіння.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.