Як ми знаємо з розділу Збирання сміття, рушій JavaScript зберігає значення в пам’яті, поки воно є “доступним” і потенційно може бути використаним.
Наприклад:
let john = { name: "Іван" };
// Об’єкт можна отримати, john -- це посилання на нього
// перезапишемо посилання
john = null;
// об’єкт буде видалено з пам’яті
Зазвичай властивості об’єкта або елементів масиву або іншої структури даних вважаються доступними та зберігаються в пам’яті, поки та структура даних є в пам’яті.
Наприклад, якщо ми покладемо об’єкт в масив, то, поки масив живий, об’єкт буде живим, навіть якщо немає інших посилань на цей об’єкт.
Ось так:
let john = { name: "Іван" };
let array = [ john ];
john = null; // перезапишемо посилання
// об’єкт, на який раніше посилалася змінна john, зберігається всередині масиву
// тому він не буде видалений збирачем сміття
// ми можемо отримати його як array[0]
Подібно до цього, якщо ми використовуємо об’єкт як ключ у звичайному Map
, то в той час, коли Map
існує, цей об’єкт також існує. Він займає пам’ять і не може бути видалений збирачем сміття.
Наприклад:
let john = { name: "Іван" };
let map = new Map();
map.set(john, "...");
john = null; // перезапишемо посилання
// john зберігається всередині map,
// ми можемо отримати його, використовуючи map.keys()
WeakMap
– принципово відрізняється в цьому аспекті. Він не перешкоджає збиранню сміття серед об’єктів, що є ключами.
Подивимося, що це означає на прикладах.
WeakMap
Перша відмінність між Map
та WeakMap
– це те, що ключі повинні бути об’єктами, а не примітивними значеннями:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ок"); // працює (об’єкт є ключем)
// не можна використовувати рядок як ключ
weakMap.set("тест", "Ой!"); // Помилка, тому що "тест" не є об’єктом
Тепер, якщо ми використовуємо об’єкт як ключ, і немає інших посилань на цей об’єкт – його буде видалено з пам’яті (і з мапи) автоматично.
let john = { name: "Іван" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // перезапишемо посилання
// john видалено з пам’яті!
Порівняйте його зі звичайним Map
, що наведений вище. Тепер, якщо john
існує лише як ключ WeakMap
– він буде автоматично видалений з мапи (і з пам’яті).
WeakMap
не підтримує ітерацію та методи keys()
, values()
, entries()
, тому немає способу отримати всі ключі або значення від нього.
WeakMap
має лише такі методи:
Чому є таке обмеження? Це з технічних причин. Якщо об’єкт втратив всі інші посилання (наприклад, john
у коді вище), то він буде автоматично видалений збирачем сміття. Але технічно немає точних вказівок коли відбувається видалення.
Рушій JavaScript вирішує це. Він може вибрати очищення пам’яті негайно або почекати, і зробити очищення пізніше, коли трапиться більше видалень. Отже, технічно, поточна кількість елементів WeakMap
невідома. Рушій, можливо, очистив його чи ні, або зробив це частково. З цієї причини методи, які дають доступ до всіх ключів/значень не підтримуються.
Отже, де нам потрібна така структура даних?
Приклад використання: додаткові дані
Основна область застосування для WeakMap
– це зберігання додаткових даних.
Якщо ми працюємо з об’єктом, що “належить” до іншого коду, можливо навіть сторонньої бібліотеки, і хотіли б зберегти деякі дані, пов’язані з ним, що повинні існувати лише поки об’єкт живий – тоді WeakMap
цє саме те, що потрібно.
Ми покладемо дані в WeakMap
, використовуючи об’єкт як ключ, і коли об’єкт буде видалено збирачем сміття, то ці дані також автоматично зникнуть.
weakMap.set(john, "секретні документи");
// якщо об’єкт john зникне, секретні документи будуть знищені автоматично
Подивімося на приклад.
У нас є код, який зберігає кількість відвідувань користувачів. Інформація зберігається в мапі: об’єкт користувачів є ключем, а кількість відвідувань – це значення. Коли користувач зникає (його об’єкт видаляється збирачем сміття), ми більше не хочемо зберігати його кількість відвідувань.
Ось приклад функції підрахунку з Map
:
// 📁 visitsCount.js
let visitsCountMap = new Map(); // мапа: користувач => кількість відвідувань
// збільшити кількість відвідувань
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
І ось ще одна частина коду, можливо, інший файл використовує це:
// 📁 main.js
let john = { name: "Іван" };
countUser(john); // рахує його візити
// пізніше john покидає нас
john = null;
Зараз, об’єкт john
повинен бути видалений збирачем сміттям, але залишається в пам’яті тому, що це ключ visitsCountMap
.
Нам потрібно очищувати visitsCountMap
, коли ми видаляємо користувачів, інакше він буде рости в пам’яті необмежено довго. Таке очищення може стати нудним завданням у складних архітектурах.
Ми можемо уникнути цього, перейшовши на WeakMap
:
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: користувач => кількість відвідувань
// збільшити кількість відвідувань
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
Тепер ми не повинні очищати visitsCountMap
. Після того, як об’єкт john
стає недоступним будь-яким способом, за винятком того, як ключ WeakMap
, він видаляється з пам’яті, разом з інформацією за цим ключем в WeakMap
.
Приклад використання: кешування
Іншим загальним прикладом є кешування. Ми можемо зберігати (“кеш”) результати з функції, щоб майбутні виклики на тому ж об’єкті могли повторно використовувати його.
Щоб досягти цього, ми можемо використовувати Map
(не оптимальний сценарій):
// 📁 cache.js
let cache = new Map();
// обчислити та запам’ятати результат
function process(obj) {
if (!cache.has(obj)) {
let result = /* розрахунки результату для */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// Тепер ми використовуємо process() в іншому файлі:
// 📁 main.js
let obj = {/* скажімо, у нас є об’єкт */};
let result1 = process(obj); // розраховано
// ...пізніше, з іншого місця в коді...
let result2 = process(obj); // запам’ятований результат, взятий з кешу
// ...пізніше, коли об’єкт більше не потрібний:
obj = null;
alert(cache.size); // 1 (Ой! Об’єкт досі в кеші і займає пам’ять!)
Для багаторазових викликів process(obj)
з тим самим об’єктом він лише обчислює результат вперше, а потім просто бере його з cache
. Недоліком є те, що нам потрібно чистити cache
, коли об’єкт більше не потрібний.
Якщо ми замінимо Map
на WeakMap
, то ця проблема зникає. Кешований результат буде видалено з пам’яті автоматично після того, як об’єкт видаляється збирачем сміття.
// 📁 cache.js
let cache = new WeakMap();
// обчислити та запам’ятати результат
function process(obj) {
if (!cache.has(obj)) {
let result = /* розрахувати результат для */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* якийсь об’єкт */};
let result1 = process(obj);
let result2 = process(obj);
// ...пізніше, коли об’єкт більше не потрібний:
obj = null;
// Не можна отримати cache.size тому, що це WeakMap,
// але це 0 або незабаром буде 0
// Коли obj видаляється збирачем сміття, кешовані дані будуть вилучені також
WeakSet
WeakSet
поводитися аналогічно:
- Це аналог
Set
, але ми можемо додати лише об’єкти доWeakSet
(не примітиви). - Об’єкт існує в наборі, коли він доступний з де-небудь ще.
- Так само як
Set
, він підтримуєadd
,has
іdelete
, але не підтримуєsize
,keys()
та ітерацію.
Будучи “слабким”, він також служить зберігання додаткових даних. Але не для довільних даних, а для фактів “так/ні”. Приналежність до WeakSet
може означати щось про об’єкт.
Наприклад, ми можемо додати користувачів до WeakSet
, щоб відстежувати тих, хто відвідав наш сайт:
let visitedSet = new WeakSet();
let john = { name: "Іван" };
let pete = { name: "Петро" };
let mary = { name: "Марія" };
visitedSet.add(john); // Іван відвідав нас
visitedSet.add(pete); // Потім Петро
visitedSet.add(john); // Знову Іван
// visitedSet має зараз 2-ох користувачів
// перевірте, чи відвідав Іван?
alert(visitedSet.has(john)); // true
// перевірте, чи відвідала Марія?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet буде очищено автоматично
Найбільш помітним обмеженням WeakMap
та WeakSet
є відсутність ітерацій та нездатність отримати весь поточний вміст. Це може виявитися незручним, але не перешкоджає WeakMap/WeakSet
виконувати свою основну роботу – бути «додатковим» сховищем даних для об’єктів, які зберігаються/управляються в іншому місці.
Підсумки
WeakMap
– це подібна до Map
колекція, яка дозволяє використовувати лише об’єкти, як ключі і видаляє їх разом з пов’язаним значенням, коли вони стануть недоступними іншим засобам.
WeakSet
– це подібна до Set
колекція, яка зберігає тільки об’єкти та видаляє їх після того, як вони стануть недоступними іншим засобам.
Їх основна перевага полягає у тому, що вони мають слабке посилання на об’єкти, тому вони можуть бути легко видаленими збирачем сміття.
Це досягається внаслідок відсутності підтримки clear
, size
, keys
, values
…
WeakMap
та WeakSet
використовуються як “вторинні” структури даних, на додаток до “первинного” сховища об’єктів. Після того, як об’єкт видаляється з первинного сховища, якщо він виявляється лише як ключ WeakMap
або в WeakSet
, він буде очищений автоматично.