W3docs

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 / SetWeakMap / WeakSet
Ключи / элементыЛюбые значенияТолько объекты
Сила ссылкиСильная (удерживает ключи)Слабая (допускает GC)
size, clear()ДаНет
Итерация (forEach, keys…)ДаНет
Лучше всего подходит дляОбщих коллекцийДанных, связанных с объектами, с автоматической очисткой

Выбирайте слабый вариант только тогда, когда вам специально нужно, чтобы движок управлял очисткой за вас, и вам не нужно перечислять содержимое. Во всех остальных случаях используйте обычные Map и Set.

Практика

Практика
Каковы ключевые характеристики WeakMap и WeakSet в JavaScript?
Каковы ключевые характеристики WeakMap и WeakSet в JavaScript?
Was this page helpful?