JavaScript WeakMap и WeakSet
В этой главе подробно рассматриваются WeakMap и WeakSet — специализированные структуры данных JavaScript для хранения слабых ссылок на объекты.
Введение в JavaScript WeakMap и WeakSet
WeakMap и WeakSet — это специализированные версии Map и Set. Они выглядят почти одинаково в использовании, но отличаются в одном принципиальном аспекте: они удерживают свои ключи (или элементы) слабо. Слабая ссылка не препятствует удалению объекта из памяти движком JavaScript. Если единственное, что указывает на объект, — это ключ WeakMap или элемент WeakSet, объект всё равно может быть удалён сборщиком мусора.
Именно это свойство определяет назначение WeakMap и WeakSet: они позволяют связывать дополнительные данные с объектом без управления его временем жизни. Когда объект исчезает, связанные с ним данные исчезают вместе с ним — автоматически, без какого-либо кода очистки.
В этой главе рассматриваются правила, которые отличают их от обычных коллекций, связанные с этим компромиссы и реальные ситуации, в которых они являются правильным инструментом.
Что означает «слабый»
В обычном Map ключ является сильной ссылкой. Пока существует Map, каждый хранящийся в нём ключ остаётся живым — даже если никакая другая часть программы его не использует. Это распространённый источник утечек памяти: вы забываете вызвать delete для записи, и объект-ключ живёт бесконечно.
WeakMap меняет это. Ключ хранится слабо, поэтому движок вправе освободить объект-ключ, как только ничто другое не ссылается на него. Когда это происходит, запись просто исчезает из WeakMap.
let visits = new WeakMap();
let user = { name: "Alice" };
visits.set(user, 10);
console.log(visits.get(user)); // 10
console.log(visits.has(user)); // true
user = null; // the object is now unreachable elsewhere
// The WeakMap entry becomes eligible for garbage collection.
// You cannot observe exactly when it is removed.WeakMap
WeakMap — это коллекция пар ключ/значение, в которой ключи должны быть объектами, а значения могут быть любыми.
Ключи должны быть объектами
Примитивные значения (string, числа, boolean, symbol, null, undefined) не могут быть ключами. Причина вытекает из устройства структуры: примитивы не подлежат сборке мусора так, как объекты, поэтому «слабое удержание» их лишено смысла. Попытка использовать примитив в качестве ключа вызывает TypeError.
let wm = new WeakMap();
wm.set("a string", 1); // TypeError: Invalid value used as weak map keyДоступные методы
WeakMap поддерживает только четыре операции:
set(key, value)— сохранить значение под ключом-объектом.get(key)— получить значение илиundefined, если ключ отсутствует.has(key)— проверить, существует ли ключ.delete(key)— удалить запись.
let wm = new WeakMap();
let key = { id: 1 };
wm.set(key, "data");
console.log(wm.has(key)); // true
console.log(wm.get(key)); // "data"
wm.delete(key);
console.log(wm.has(key)); // falseНет итерации и нет размера
У WeakMap нет свойства size, нет clear() и нет способа перебрать его содержимое (нет keys(), values(), entries() или forEach). Это не упущение. Поскольку записи могут исчезнуть в любой момент при запуске сборщика мусора, содержимое недетерминировано — его раскрытие позволило бы коду наблюдать за работой сборщика, что язык намеренно скрывает.
let wm = new WeakMap();
console.log("size" in wm); // false
console.log(wm[Symbol.iterator]); // undefinedЕсли вам нужно считать записи, перебирать их или хранить примитивные ключи, используйте обычный Map.
WeakSet
WeakSet — это аналог Set: коллекция уникальных объектов, которые удерживаются слабо. Как и WeakMap, его элементы должны быть объектами, и он не предоставляет итерации или size.
let visited = new WeakSet();
let a = { id: 1 };
let b = { id: 2 };
visited.add(a);
console.log(visited.has(a)); // true
console.log(visited.has(b)); // false
visited.delete(a);
console.log(visited.has(a)); // falseЕго полный API состоит из add(value), has(value) и delete(value).
Практические сценарии использования
Кэширование и мемоизация
Кэшируйте результат ресурсоёмкого вычисления, используя связанный объект в качестве ключа. Поскольку ключ слабый, кэш никогда не удерживает объект, который больше не нужен.
const cache = new WeakMap();
function process(obj) {
if (cache.has(obj)) {
return cache.get(obj); // reuse the cached result
}
const result = obj.value * 2; // pretend this is expensive
cache.set(obj, result);
return result;
}
let data = { value: 21 };
console.log(process(data)); // 42 (computed)
console.log(process(data)); // 42 (from cache)Приватные данные объекта
Храните данные, принадлежащие объекту, не добавляя свойство к самому объекту (которое кто угодно мог бы прочитать или перезаписать). Это классический способ эмулировать по-настоящему приватные поля, связанный с тем, как методы привязываются к объектам через this.
const balances = new WeakMap();
class Account {
constructor(amount) {
balances.set(this, amount); // private to this module
}
deposit(n) {
balances.set(this, balances.get(this) + n);
}
get balance() {
return balances.get(this);
}
}
const acc = new Account(100);
acc.deposit(50);
console.log(acc.balance); // 150
// There is no `amount` property on `acc` itself to tamper with.Когда экземпляр Account больше не используется, его запись с балансом собирается автоматически.
Метаданные для DOM-узлов
Частая утечка памяти на долго живущих страницах — хранение Map метаданных DOM-узлов после их удаления из документа. С WeakSet/WeakMap как только узел отсоединён и разыменован, его метаданные тоже освобождаются.
const seen = new WeakSet();
function markVisited(node) {
if (seen.has(node)) return false; // already processed
seen.add(node);
return true;
}
// When the node leaves the DOM and nothing else holds it,
// it disappears from `seen` without manual cleanup.WeakMap/WeakSet против Map/Set
| Возможность | Map / Set | WeakMap / WeakSet |
|---|---|---|
| Ключи / элементы | Любые значения | Только объекты |
| Сила ссылки | Сильная (удерживает ключи) | Слабая (допускает GC) |
size, clear() | Да | Нет |
Итерация (forEach, keys…) | Да | Нет |
| Лучше всего подходит для | Общих коллекций | Данных, связанных с объектами, с автоматической очисткой |
Выбирайте слабый вариант только тогда, когда вам специально нужно, чтобы движок управлял очисткой за вас, и вам не нужно перечислять содержимое. Во всех остальных случаях используйте обычные Map и Set.