Каковы фактические использования ES6 WeakMap?

397

Каковы фактические виды использования WeakMapструктуры данных, представленной в ECMAScript 6?

Поскольку ключ слабой карты создает сильную ссылку на его соответствующее значение, гарантируя, что значение, которое было вставлено в слабую карту, никогда не исчезнет, ​​пока его ключ еще жив, его нельзя использовать для мемо-таблиц, кэши или что-то еще, для чего вы обычно используете слабые ссылки, карты со слабыми значениями и т. д.

Мне кажется, что это:

weakmap.set(key, value);

... это просто окольный способ сказать это:

key.value = value;

Какие конкретные варианты использования я пропускаю?

valderman
источник
10
Сообщение в блоге.
Заостренный
2
И еще один - ilikekillnerds.com/2015/02/what-are-weakmaps-in-es6
Джеймс Самнерс
35
Реальный пример использования: хранить пользовательские данные для узлов DOM.
Феликс Клинг
Все случаи использования, которые вы упоминаете для слабых ссылок, также очень важны. Их просто сложнее добавить в язык, поскольку они вводят недетерминизм. Марк Миллер и другие проделали большую работу над слабыми ссылками, и я думаю, что они в конечном итоге придут. В конце концов
Бенджамин Грюнбаум
2
WeakMaps можно использовать для обнаружения утечек памяти: stevehanov.ca/blog/?id=148
theWebalyst

Ответы:

513

В корне

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

WeakMap - это карта (словарь), в которой ключи слабы - то есть, если все ссылки на ключ потеряны и больше нет ссылок на значение - значение может быть собрано мусором. Давайте сначала покажем это на примерах, затем немного объясним и, наконец, закончим с реальным использованием.

Допустим, я использую API, который дает мне определенный объект:

var obj = getObjectFromLibrary();

Теперь у меня есть метод, который использует объект:

function useObj(obj){
   doSomethingWith(obj);
}

Я хочу отслеживать, сколько раз метод вызывался с определенным объектом, и сообщать, если это произошло более чем в N раз. Наивно можно подумать, чтобы использовать карту:

var map = new Map(); // maps can have object keys
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

Это работает, но имеет утечку памяти - теперь мы отслеживаем каждый отдельный объект библиотеки, передаваемый в функцию, которая предотвращает сборку мусора объектами библиотеки. Вместо этого - мы можем использовать WeakMap:

var map = new WeakMap(); // create a weak map
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // Report called more than 10 times
    map.set(obj, called);
}

И утечка памяти исчезла.

Случаи использования

Некоторые варианты использования, которые в противном случае могли бы вызвать утечку памяти и включаются WeakMapс помощью s, включают:

  • Хранение личных данных о конкретном объекте и предоставление доступа к нему только людям со ссылкой на карту. Более специальный подход идет с предложением частных символов, но это еще долго.
  • Хранение данных об объектах библиотеки без их изменения или накладных расходов.
  • Хранение данных о небольшом наборе объектов, где существует много объектов этого типа, чтобы не было проблем со скрытыми классами, которые движки JS используют для объектов того же типа.
  • Хранение данных о хост-объектах, таких как DOM-узлы, в браузере.
  • Добавление возможности к объекту извне (как в примере источника событий в другом ответе).

Давайте посмотрим на реальное использование

Он может быть использован для расширения объекта снаружи. Давайте приведем практический (адаптированный, в некотором роде реальный - чтобы подчеркнуть) пример из реального мира Node.js.

Допустим, вы Node.js и у вас есть Promiseобъекты - теперь вы хотите отслеживать все отклоненные в настоящий момент обещания - однако вы не хотите, чтобы они не собирались мусором, если на них не существует ссылок.

Теперь вы не хотите добавлять свойства к нативным объектам по понятным причинам - так что вы застряли. Если вы сохраняете ссылки на обещания, вы вызываете утечку памяти, поскольку сборка мусора невозможна. Если вы не сохраните ссылки, вы не сможете сохранить дополнительную информацию об отдельных обещаниях. Любая схема, которая предполагает сохранение идентификатора обещания, означает, что вам нужна ссылка на него.

Введите WeakMaps

Слабые карты означают, что ключи слабые. Нет способов перечислить слабую карту или получить все ее значения. На слабой карте вы можете хранить данные, основанные на ключе, а когда ключ получает мусор, собирайте значения.

Это означает, что с помощью обещания вы можете сохранить состояние о нем, и этот объект все еще можно собирать мусором. Позже, если вы получите ссылку на объект, вы можете проверить, есть ли у вас какое-либо состояние, относящееся к нему, и сообщить об этом.

Это было использовано для реализации необработанных крюков отказов от Петька Антонов , как это :

process.on('unhandledRejection', function(reason, p) {
    console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
    // application specific logging, throwing an error, or other logic here
});

Мы храним информацию об обещаниях на карте и можем знать, когда было выполнено отклоненное обещание.

Бенджамин Грюнбаум
источник
8
Здравствуйте! Не могли бы вы сказать мне, какая часть кода примера вызывает утечку памяти?
Итамайс
15
@ ltamajs4, конечно, в useObjпримере, использующем a, Mapа не a, WeakMapмы используем переданный объект как ключ карты. Объект никогда не удаляется с карты (так как мы не знали бы, когда это сделать), поэтому всегда есть ссылка на него, и он никогда не может быть собран мусором. В примере WeakMap, как только все другие ссылки на объект исчезнут, объект может быть очищен от WeakMap. Если вы все еще не уверены, что я имею в виду, пожалуйста, дайте мне знать
Бенджамин Грюнбаум
@ Benjamin, нам нужно различать потребность в чувствительном к памяти кэше и необходимость в кортеже data_object. Не объединяйте эти два отдельных требования. Ваш calledпример лучше написан с использованием jsfiddle.net/f2efbm7z, и он не демонстрирует использование слабой карты. Фактически, это может быть лучше написано в общей сложности 6 способами, которые я перечислю ниже.
Pacerier
По сути, целью слабой карты является чувствительный к памяти кэш. Хотя он может быть использован для расширения объектов извне, это ненужный паршивый хак и определенно не является его надлежащим назначением .
Pacerier
1
Если вы хотите сохранить связь между обещанием и количеством раз, когда оно было обработано / отклонено, используйте 1) символ; p[key_symbol] = data, или 2) уникальное именование; p.__key = data, или 3) частная сфера; (()=>{let data; p.Key = _=>data=_;})(), или 4) прокси с 1 или 2 или 3. или 5) заменить / расширить класс Promise на 1, 2 или 3. или 6) заменить / расширить класс Promise с набором необходимых членов. - В любом случае, слабая карта не нужна, если вам не нужен кэш, чувствительный к памяти.
Pacerier
48

Этот ответ кажется предвзятым и непригодным для сценария реального мира. Пожалуйста, прочитайте это как есть, и не рассматривайте это как актуальный вариант для чего-либо другого, кроме экспериментов

Вариант использования может быть использовать его в качестве словаря для слушателей, у меня есть сотрудник, который сделал это. Это очень полезно, потому что любой слушатель напрямую нацелен на такой способ работы. До свидания listener.on.

Но с более абстрактной точки зрения, WeakMapособенно мощно дематериализовать доступ практически ко всему, вам не нужно пространство имен, чтобы изолировать его членов, поскольку это уже подразумевается природой этой структуры. Я почти уверен, что вы могли бы сделать некоторые значительные улучшения памяти, заменив неловкие избыточные ключи объектов (хотя деконструкция работает для вас).


Прежде чем читать, что дальше

Теперь я понимаю, что мои акценты не совсем лучший способ решения проблемы, и, как отметил Бенджамин Грюнбаум (посмотрите его ответ, если он еще не выше моего: p), эту проблему нельзя было бы решить с помощью регулярного Map, так как он просочился бы, поэтому главная сила в WeakMapтом, что он не мешает сбору мусора, поскольку они не сохраняют ссылку.


Вот актуальный код моего коллеги (спасибо ему за то, что поделился)

Полный источник здесь , это об управлении слушателями, о котором я говорил выше (вы также можете взглянуть на спецификации )

var listenableMap = new WeakMap();


export function getListenable (object) {
    if (!listenableMap.has(object)) {
        listenableMap.set(object, {});
    }

    return listenableMap.get(object);
}


export function getListeners (object, identifier) {
    var listenable = getListenable(object);
    listenable[identifier] = listenable[identifier] || [];

    return listenable[identifier];
}


export function on (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    listeners.push(listener);
}


export function removeListener (object, identifier, listener) {
    var listeners = getListeners(object, identifier);

    var index = listeners.indexOf(listener);
    if(index !== -1) {
        listeners.splice(index, 1);
    }
}


export function emit (object, identifier, ...args) {
    var listeners = getListeners(object, identifier);

    for (var listener of listeners) {
        listener.apply(object, args);
    }
}
axelduch
источник
2
Я не совсем понимаю, как вы будете использовать это. Это приведет к коллапсу наблюдаемого вместе с событиями, связанными с ним, когда на него больше не ссылаются. Проблема, с которой я обычно сталкиваюсь, заключается в том, что на Observer больше нет ссылок. Я думаю, что решение здесь решило только половину проблемы. Я не думаю, что вы можете решить проблему наблюдателя с помощью WeakMap, поскольку она не повторяется.
jgmjgm
1
Слушатели событий с двойной буферизацией могут быть быстрыми на других языках, но в этом случае они просто эзотерические и медленные. Это мои три цента.
Джек Гиффин
@axelduch, Ух ты, этот миф о дескрипторе слушателя распространялся всюду по сообществу Javascript, получая 40 голосов! Чтобы понять, почему этот ответ совершенно неправильный , см. Комментарии в stackoverflow.com/a/156618/632951
Pacerier
1
@Pacerier обновил ответ, спасибо за отзыв
axelduch
1
@axelduch, да, там тоже есть ссылка.
Pacerier
18

WeakMap хорошо работает для инкапсуляции и сокрытия информации

WeakMapдоступно только для ES6 и выше. A WeakMapпредставляет собой набор пар ключ и значение, где ключ должен быть объектом. В следующем примере мы создаем WeakMapдва элемента:

var map = new WeakMap();
var pavloHero = {first: "Pavlo", last: "Hero"};
var gabrielFranco = {first: "Gabriel", last: "Franco"};
map.set(pavloHero, "This is Hero");
map.set(gabrielFranco, "This is Franco");
console.log(map.get(pavloHero));//This is Hero

Мы использовали set()метод для определения связи между объектом и другим элементом (в нашем случае это строка). Мы использовали get()метод для получения элемента, связанного с объектом. Интересным аспектом WeakMaps является тот факт, что он содержит слабую ссылку на ключ внутри карты. Слабая ссылка означает, что если объект уничтожен, сборщик мусора удалит всю запись из WeakMap, тем самым освободив память.

var TheatreSeats = (function() {
  var priv = new WeakMap();
  var _ = function(instance) {
    return priv.get(instance);
  };

  return (function() {
      function TheatreSeatsConstructor() {
        var privateMembers = {
          seats: []
        };
        priv.set(this, privateMembers);
        this.maxSize = 10;
      }
      TheatreSeatsConstructor.prototype.placePerson = function(person) {
        _(this).seats.push(person);
      };
      TheatreSeatsConstructor.prototype.countOccupiedSeats = function() {
        return _(this).seats.length;
      };
      TheatreSeatsConstructor.prototype.isSoldOut = function() {
        return _(this).seats.length >= this.maxSize;
      };
      TheatreSeatsConstructor.prototype.countFreeSeats = function() {
        return this.maxSize - _(this).seats.length;
      };
      return TheatreSeatsConstructor;
    }());
})()
Михаил Хорожанский
источник
4
«Слабая карта хорошо работает для инкапсуляции и сокрытия информации». То, что ты можешь, не означает, что ты должен. В Javascript есть стандартные способы инкапсуляции и сокрытия информации еще до изобретения уязвимости. На данный момент существует буквально 6 способов сделать это . Использование уязвимости для инкапсуляции - это ужасный лицевой щиток лица.
Pacerier
12

𝗠𝗲𝘁𝗮𝗱𝗮𝘁𝗮

Слабые карты могут использоваться для хранения метаданных об элементах DOM, не мешая сбору мусора и не сводя коллег с ума от вашего кода. Например, вы можете использовать их для нумерации всех элементов веб-страницы.

𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀 𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀 𝗼𝗿 𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀:

var elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length;

while (++i !== len) {
  // Production code written this poorly makes me want to cry:
  elements[i].lookupindex = i;
  elements[i].elementref = [];
  elements[i].elementref.push( elements[(i * i) % len] );
}

// Then, you can access the lookupindex's
// For those of you new to javascirpt, I hope the comments below help explain 
// how the ternary operator (?:) works like an inline if-statement
document.write(document.body.lookupindex + '<br />' + (
    (document.body.elementref.indexOf(document.currentScript) !== -1)
    ? // if(document.body.elementref.indexOf(document.currentScript) !== -1){
    "true"
    : // } else {
    "false"
  )   // }
);

𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀 𝗪𝗲𝗮𝗸𝗠𝗮𝗽𝘀 𝗮𝗻𝗱 𝗪𝗲𝗮𝗸𝗦𝗲𝘁𝘀:

var DOMref = new WeakMap(),
  __DOMref_value = Array,
  __DOMref_lookupindex = 0,
  __DOMref_otherelement = 1,
  elements = document.getElementsByTagName('*'),
  i = -1, len = elements.length, cur;

while (++i !== len) {
  // Production code written this greatly makes me want to 😊:
  cur = DOMref.get(elements[i]);
  if (cur === undefined)
    DOMref.set(elements[i], cur = new __DOMref_value)

  cur[__DOMref_lookupindex] = i;
  cur[__DOMref_otherelement] = new WeakSet();
  cur[__DOMref_otherelement].add( elements[(i * i) % len] );
}

// Then, you can access the lookupindex's
cur = DOMref.get(document.body)
document.write(cur[__DOMref_lookupindex] + '<br />' + (
    cur[__DOMref_otherelement].has(document.currentScript)
    ? // if(cur[__DOMref_otherelement].has(document.currentScript)){
    "true"
    : // } else {
    "false"
  )   // }
);

𝗧𝗵𝗲 𝗗𝗶𝗳𝗳𝗲𝗿𝗲𝗻𝗰𝗲

Разница может показаться незначительной, за исключением того, что версия слабой карты длиннее, однако между двумя фрагментами кода, показанными выше, есть существенная разница. В первом фрагменте кода, без слабых отображений, фрагмент кода хранит ссылки в разные стороны между элементами DOM. Это предотвращает сборку мусора элементами DOM.(i * i) % lenможет показаться странным, что никто бы не использовал, но подумайте еще раз: во многих производственных кодах есть ссылки на DOM, которые отражаются по всему документу. Теперь для второго фрагмента кода, поскольку все ссылки на элементы являются слабыми, когда вы удаляете узел, браузер может определить, что узел не используется (не может быть достигнут вашим кодом), и таким образом удалите это из памяти. Причина, по которой вы должны быть обеспокоены использованием памяти и якорями памяти (такими как первый фрагмент кода, где неиспользуемые элементы хранятся в памяти), заключается в том, что большее использование памяти означает больше попыток GC браузера (чтобы попытаться освободить память для предотвращение сбоя браузера) означает более медленный просмотр и иногда сбой браузера.

Что касается polyfill для них, я бы порекомендовал мою собственную библиотеку ( находится здесь @ github ). Это очень легкая библиотека, которая просто заполняет ее без каких-либо слишком сложных сред, которые вы можете найти в других полизаполнениях.

Счастливого кодирования!

Джек Гиффин
источник
1
Спасибо за четкое объяснение. Пример стоит больше, чем любые слова.
newguy
@lolzery, Re « Это предотвращает сборку мусора для элементов DOM », все, что вам нужно, это установить elementsна ноль, и все готово: это будет GCed. & Re: « Ссылки DOM, которые отражаются по всему документу », не имеет никакого значения: как только основная ссылка elementsисчезнет, ​​все циклические ссылки будут GCed. Если ваш элемент содержит ссылки на элемент, который ему не нужен, то исправьте код и установите для ref значение null, когда вы закончите с его использованием. Это будет GCed. Слабые карты не нужны .
Pacerier
2
@Pacerier спасибо за ваш восторженный отзыв, однако установка elementsна ноль не позволит браузеру собирать элементы в ситуации первого фрагмента. Это происходит потому, что вы устанавливаете пользовательские свойства для элементов, и затем эти элементы все еще могут быть получены, и к их пользовательским свойствам все равно можно получить доступ, тем самым предотвращая GC-кодирование любого из них. Думайте об этом как о цепочке металлических колец. Solongas у вас есть доступ по крайней мере к одному звену в цепочке, вы можете удерживать это звено в цепочке и, таким образом, предотвращать падение всей цепочки предметов в пропасть.
Джек Гиффин
1
производственный код с dunder по имени vars вызывает у меня рвоту
Барбу Барбу
10

Я использую WeakMapдля кеша беспроблемное запоминание функций, которые принимают неизменяемые объекты в качестве своих параметров.

Мемоизация - это причудливый способ сказать: «после того, как вы вычислите значение, кэшируйте его, чтобы вам не приходилось вычислять его снова».

Вот пример:

Несколько вещей, на которые стоит обратить внимание:

  • Объекты Immutable.js возвращают новые объекты (с новым указателем), когда вы изменяете их, поэтому использование их в качестве ключей в WeakMap гарантирует то же вычисленное значение.
  • WeakMap отлично подходит для заметок, потому что как только объект (используемый в качестве ключа) получает сборщик мусора, то же самое происходит и с вычисленным значением в WeakMap.
Рико Калер
источник
1
Это допустимое использование слабой карты, если кэш памяти должен быть чувствительным к памяти , а не постоянным в течение всей жизни объекта / функции. Если предполагается, что «кэш-память» является постоянным в течение всей жизни объекта / функции, то вместо этого карты выступают weakmap: используйте вместо этого любой из 6 методов инкапсуляции javascript по умолчанию .
Pacerier
3

У меня есть этот простой основанный на функции вариант использования / Пример для WeakMaps.

УПРАВЛЯЙТЕ КОЛЛЕКЦИЕЙ ПОЛЬЗОВАТЕЛЕЙ

Я начал с Userобъектом, свойство которого включает в себя fullname, username, age, genderи метод , называемом , printкоторый печатает удобочитаемое резюме других свойств.

/**
Basic User Object with common properties.
*/
function User(username, fullname, age, gender) {
    this.username = username;
    this.fullname = fullname;
    this.age = age;
    this.gender = gender;
    this.print = () => console.log(`${this.fullname} is a ${age} year old ${gender}`);
}

Затем я добавил карту, вызываемую usersдля сохранения коллекции нескольких пользователей, на которые указывает ключ username.

/**
Collection of Users, keyed by username.
*/
var users = new Map();

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

/**
Creates an User Object and adds it to the users Collection.
*/
var addUser = (username, fullname, age, gender) => {
    let an_user = new User(username, fullname, age, gender);
    users.set(username, an_user);
}

/**
Returns an User Object associated with the given username in the Collection.
*/
var getUser = (username) => {
    return users.get(username);
}

/**
Deletes an User Object associated with the given username in the Collection.
*/
var deleteUser = (username) => {
    users.delete(username);
}

/**
Prints summary of all the User Objects in the Collection.
*/
var printUsers = () => {
    users.forEach((user) => {
        user.print();
    });
}

Поскольку весь вышеприведенный код работает, скажем, в NodeJS , только usersкарта имеет ссылку на пользовательские объекты во всем процессе. Нет другой ссылки на отдельные объекты пользователя.

Запустив этот код в виде интерактивной оболочки NodeJS, просто в качестве примера я добавляю четырех пользователей и печатаю их: Добавление и печать пользователей

ДОБАВЬТЕ БОЛЬШЕ ИНФОРМАЦИИ ДЛЯ ПОЛЬЗОВАТЕЛЕЙ БЕЗ ИЗМЕНЕНИЯ СУЩЕСТВУЮЩЕГО КОДА

Теперь, скажем, требуется новая функция, в которой ссылки каждого пользователя на платформу Social Media Platform (SMP) должны отслеживаться вместе с объектами пользователя.

Ключевым моментом здесь также является то, что эта функция должна быть реализована с минимальным вмешательством в существующий код.

Это возможно с WeakMaps следующим образом.

Я добавляю три отдельных WeakMaps для Twitter, Facebook, LinkedIn.

/*
WeakMaps for Social Media Platforms (SMPs).
Could be replaced by a single Map which can grow
dynamically based on different SMP names . . . anyway...
*/
var sm_platform_twitter = new WeakMap();
var sm_platform_facebook = new WeakMap();
var sm_platform_linkedin = new WeakMap();

Вспомогательная функция getSMPWeakMapдобавляется просто для того, чтобы вернуть WeakMap, связанный с данным именем SMP.

/**
Returns the WeakMap for the given SMP.
*/
var getSMPWeakMap = (sm_platform) => {
    if(sm_platform == "Twitter") {
        return sm_platform_twitter;
    }
    else if(sm_platform == "Facebook") {
        return sm_platform_facebook;
    }
    else if(sm_platform == "LinkedIn") {
        return sm_platform_linkedin;
    }
    return undefined;
}

Функция для добавления SMP-ссылки пользователя к указанному SMP WeakMap.

/**
Adds a SMP link associated with a given User. The User must be already added to the Collection.
*/
var addUserSocialMediaLink = (username, sm_platform, sm_link) => {
    let user = getUser(username);
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    if(user && sm_platform_weakmap) {
        sm_platform_weakmap.set(user, sm_link);
    }
}

Функция для печати только тех пользователей, которые присутствуют на данном SMP.

/**
Prints the User's fullname and corresponding SMP link of only those Users which are on the given SMP.
*/
var printSMPUsers = (sm_platform) => {
    let sm_platform_weakmap = getSMPWeakMap(sm_platform);
    console.log(`Users of ${sm_platform}:`)
    users.forEach((user)=>{
        if(sm_platform_weakmap.has(user)) {
            console.log(`\t${user.fullname} : ${sm_platform_weakmap.get(user)}`)
        }
    });
}

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

... продолжая предыдущий пример, я добавляю ссылки SMP для пользователей, несколько ссылок для пользователей Билла и Сары, а затем печатаю ссылки для каждого SMP отдельно: Добавление SMP-ссылок пользователям и их отображение

Теперь скажите, что пользователь удален с usersкарты по телефону deleteUser. Это удаляет единственную ссылку на пользовательский объект. Это, в свою очередь, также очистит SMP-ссылку от любого / всех SMP WeakMaps (сборщиком мусора), так как без объекта User невозможно получить доступ к какой-либо из его SMP-ссылок.

... продолжая Пример, я удаляю пользователя Билла, а затем распечатываю ссылки SMP, с которыми он был связан:

При удалении пользователя Билла с карты удаляются и ссылки SMP.

Не требуется никакого дополнительного кода для отдельного удаления ссылки SMP отдельно и существующего кода до того, как эта функция не была изменена в любом случае.

Если есть какой-либо другой способ добавить эту функцию с / без WeakMaps, пожалуйста, не стесняйтесь комментировать.

electrocrat
источник
_____ хорошая ______
Алекс