Все темы
13

WeakMap и WeakSet: привязываем данные к объектам

Tier 3Бесплатно

Где применяется

1. Задача

На прошлом шаге ты работал с Map: ключ указывает на значение. Map держит свои ключи. Пока Map существует, каждый ключ в нём тоже остаётся в памяти, даже если в остальной программе этот ключ больше не нужен. Обычно это не страшно. Но иногда так получается медленная утечка памяти: память занимается и никогда не освобождается.

Допустим, ты хочешь привязать немного данных к объектам, которые появляются и исчезают. Например, к каждому элементу на странице привязать счётчик, сколько раз его перерисовали. Или к каждому входящему запросу привязать отметку «уже проверен». Если сложить это в обычный Map, где ключ это сам объект, Map будет держать каждый такой объект в памяти вечно. Элементы со страницы убрали, запросы завершились, а Map всё ещё хранит их. Защита одна: не забыть удалить каждую запись вручную. Пропустишь одну, и память будет расти, пока работает программа.

WeakMap и WeakSet решают эту проблему. В них ключ может забыться сам.

2. Простыми словами

Возьми объект из кода ниже: элемент страницы node = { id: 1 }. Ты хочешь запомнить, что его отрисовали 3 раза, и кладёшь { renders: 3 } в WeakMap с ключом node.

Потом страница удаляет элемент, и ни одна часть программы больше не ссылается на node. С этого момента движок вправе освободить node, и запись node → { renders: 3 } пропадает вместе с ним. Строчку, которая удаляет запись, ты не писал. Общего списка всех отмеченных элементов, который кому-то пришлось бы чистить, тоже нет.

Итак, данные хранятся при объекте и исчезают вместе с ним, а шаг уборки помнить не нужно.

3. Идея

WeakMap похож на Map: ключ указывает на значение. Правило одно: ключи должны быть объектами, простые строки и числа не подходят. WeakSet похож на Set: он хранит отметку «этот объект здесь есть». Правило то же: внутрь кладут только объекты.

Слово «weak» (слабый) говорит о том, как коллекция держит ключи. Обычный Map сам по себе удерживает ключ в памяти. WeakMap ключ не удерживает. Здесь «в памяти» значит, что движок JavaScript (программа, которая выполняет твой код) ещё хранит объект. Сборка мусора это работа движка: он сам освобождает память объекта, на который больше ничего не ссылается. Если на объект ссылается только ключ WeakMap, движок может удалить объект и тихо убрать вместе с ним эту запись. Кода уборки ты не писал: запись исчезла, когда исчез объект.

У этого правила есть цена. Запись может пропасть в любой момент, когда движок решит собрать мусор. Поэтому слабая коллекция не даёт посмотреть на всё своё содержимое. Слабые коллекции нельзя перебрать, и у них нет size. Их нельзя пройти циклом и нельзя спросить, сколько в них записей. Можно спросить только про объект, который у тебя уже есть.

Поэтому операций у них мало:

const meta = new WeakMap();
const node = { id: 1 };          // любой объект годится как ключ
meta.set(node, { renders: 3 });  // привязали данные к нему
meta.get(node);                  // { renders: 3 }
meta.has(node);                  // true
meta.delete(node);               // можно убрать и вручную, раньше

const seen = new WeakSet();
seen.add(node);                  // отметили этот объект
seen.has(node);                  // true

Когда node больше нигде не используется, и запись в meta, и отметка в seen могут исчезнуть. Вызывать для этого delete не нужно.

пока узел в ходу: node node → {r:3} после того как узел убрали: (нет) (нет) запись ушла вместе с ключом, без вызова delete().

Одна честная оговорка перед практикой. Сборка мусора идёт по расписанию самого движка. Заставить её запуститься нельзя, увидеть из теста тоже нельзя. Поэтому упражнение ниже не пытается доказать, что записи исчезают. Оно тренирует то, что можно проверить: умение выбрать, какая из четырёх коллекций нужна в конкретной ситуации.

4. Во что это обходится

set, get, has и delete стоят примерно по одному шагу, как у Map и Set. Память O(n), где n это число записей, чьи ключи ещё используются. Выигрыш в том, за что платить не нужно. Как только объект-ключ больше нигде не используется, его запись перестаёт занимать твою память, и кода для этого ты не писал. Забытый delete здесь не приведёт к утечке, потому что delete вообще не нужен.

Цена в ограничениях. Ключи должны быть объектами, поэтому строку с id пользователя или число ключом сделать нельзя. Перебора и size нет, поэтому WeakMap не подходит, когда нужно перечислить или посчитать содержимое. В таких случаях возвращайся к обычному Map или Set из двух прошлых шагов. Уборка тогда снова на тебе.

6. Где это встречается

Кэш (сохранённый результат вычисления), где ключом служит объект запроса или конфига, из которого получен результат. Запись в кэше пропадает вместе с объектом, и вычищать её не нужно. Приватные поля в библиотеке классов: WeakMap на уровне модуля сопоставляет каждому экземпляру его скрытое состояние, до которого снаружи не добраться. Отметки об объектах, которые конвейер обработки уже прошёл, без удержания этих объектов в памяти после работы. Библиотеки, которые привязывают служебные данные к переданным им объектам, например к узлам страницы (элементам, из которых браузер строит страницу). Обычный Map держал бы твои объекты в памяти всё время жизни библиотеки. Когда данные принадлежат объекту и должны жить ровно столько же, сколько он, WeakMap и WeakSet убирают их за тебя.

На LeetCode

На LeetCode нет задач, где нужен WeakMap или WeakSet, ведь тесты там не меряют память. Навык из урока, выбор одной из четырёх коллекций, проверяют задачи на Map и Set из уроков 11, 12 и 18.

Твоя задача (Tier 3)

В этом шаге больше чтения, чем кода. Прочитай правило, потом допиши pickCollection(scenario).

Ты получаешь объект с описанием ситуации. Верни название подходящей коллекции: "WeakMap", "WeakSet", "Map" или "Set".

  • Вход: scenario, объект с тремя флагами «да/нет»: keysAreObjects (ключи это объекты), storesValue (под ключом хранится значение) и forgetWhenUnused (запись должна забываться сама).
  • Возвращает: одно из "WeakMap", "WeakSet", "Map", "Set".

Шаги:

  1. Сначала реши, подходит ли слабая коллекция. Она подходит, только когда keysAreObjects истинно И forgetWhenUnused истинно.
  2. Если слабая подходит: верни "WeakMap", когда storesValue истинно, иначе "WeakSet".
  3. Если нет: верни "Map", когда storesValue истинно, иначе "Set".

Готово, когда проходят все тесты:

  • ключи это объекты, хранится значение, нужно автозабывание -> "WeakMap"
  • ключи это текст, только отметка «есть», автозабывание не нужно -> "Set"
  • ключи это объекты, только отметка «есть», нужно автозабывание -> "WeakSet"
  • ключи это текст, хранится значение, автозабывание не нужно -> "Map"
  • ключи это объекты, только отметка «есть», автозабывание не нужно -> "Set"
  • нужно автозабывание, но ключи это текст -> "Map" (слабая коллекция не принимает текстовые ключи, так что убирать записи придётся тебе)

Практика

pickCollection (Map / Set / WeakMap / WeakSet)

export function pickCollection(scenario) {
  // Your code here. The task is described in the box above the editor.
}

Ты здесь как гость

Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.

Лестница подсказок

Застрял? Открывай по одной, но сначала попробуй сам.

    Мы используем необходимые куки для работы сайта (вход и язык). Если ты согласишься, мы также загрузим Google Analytics, чтобы видеть, какие страницы читают, и Google reCAPTCHA для защиты форм обратной связи и сообщений об ошибке от спама. Политика конфиденциальности