Які фактичні можливості використання ES6 WeakMap?


397

Які фактичні можливості використання WeakMapструктури даних, введеної в ECMAScript 6?

Оскільки ключ слабкої карти створює чітке посилання на його відповідне значення, гарантуючи, що значення, вставлене у слабку карту, ніколи не зникне, доки його ключ ще живий, його не можна використовувати для таблиць нагадувань, кеші або що-небудь інше, для чого зазвичай використовуються слабкі посилання, карти зі слабкими значеннями тощо.

Мені здається, що це:

weakmap.set(key, value);

... це лише круговий спосіб сказати це:

key.value = value;

Які конкретні випадки використання я пропускаю?




35
Випадок використання в реальному світі: зберігайте власні дані для DOM-вузлів.
Фелікс Клінг

Усі випадки використання, які ви згадуєте для слабких посилань, теж надто важливі. Їх просто набагато складніше додати до мови, оскільки вони запроваджують недетермінізм. Марк Міллер та інші провели багато роботи над слабкими посиланнями, і я думаю, що вони зрештою приходять. Врешті-решт
Бенджамін Грюнбаум

2
WeakMaps можна використовувати для виявлення витоків пам'яті: stevehanov.ca/blog/?id=148
theWebalyst

Відповіді:


513

Принципово

Слабкі карти надають спосіб поширити предмети ззовні, не перешкоджаючи збору сміття. Щоразу, коли ви хочете розширити об'єкт, але не можете, оскільки він запечатаний - або із зовнішнього джерела - може бути застосований слабкий карту.

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);
}

І витоку пам’яті вже немає.

Використовуйте випадки

Деякі випадки використання, які в іншому випадку спричинили б витік пам'яті та були включені WeakMaps, включають:

  • Зберігання приватних даних про певний об’єкт та надання доступу до них лише людям із посиланням на карту. З пропозицією приватних символів пропонується більш спеціальний підхід, але це вже давно.
  • Зберігання даних про об’єкти бібліотеки, не змінюючи їх і не маючи накладних витрат.
  • Зберігання даних про невеликий набір об'єктів, де існує безліч об'єктів типу, щоб не виникало проблем із прихованими класами, які JS-двигуни використовують для об'єктів одного типу.
  • Зберігання даних про об'єкти хоста, як-от вузли DOM у браузері.
  • Додавання можливості до об'єкта ззовні (як приклад випромінювача події в іншій відповіді).

Давайте розглянемо реальне використання

Він може використовуватися для розширення предмета ззовні. Наведемо практичний (адаптований, такий собі реальний - щоб зробити крапку) приклад із реального світу Node.js.

Скажімо, ви Node.js і у вас є Promiseоб’єкти - тепер ви хочете відслідковувати всі відхилені на даний момент обіцянки - однак, ви не хочете утримувати їх від збирання сміття, якщо на них немає посилань.

Тепер ви не хочете додавати властивості до рідних об'єктів з зрозумілих причин - тому ви застрягли. Якщо ви дотримуєтесь посилань на обіцянки, ви спричиняєте витік пам’яті, оскільки жоден сміття не може відбутися Якщо ви не зберігаєте посилання, то ви не можете зберегти додаткову інформацію про окремі обіцянки. Будь-яка схема, яка передбачає збереження посвідчення обіцянки по суті, означає, що вам потрібно посилання на неї.

Введіть слабкі карти

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

Це означає, що, даючи обіцянку, ви можете зберігати стан про це - і цей об’єкт все ще можна збирати сміття. Пізніше, якщо ви отримаєте посилання на об'єкт, ви можете перевірити, чи є у вас стан, пов'язаний з ним, і повідомити про нього.

Це було використано для реалізації неподілених гачків відхилення Петка Антонова таким чином :

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
Привіт! Скажіть, будь ласка, яка частина прикладу коду викликає протікання пам'яті?
ltamajs

15
@ ltamajs4 впевнений, що в useObjприкладі, використовуючи a, Mapа не a, WeakMapми використовуємо переданий об'єкт у вигляді ключа карти. Об'єкт ніколи не видаляється з карти (оскільки ми не знаємо, коли це робити), тому на нього завжди посилається, і він ніколи не може збирати сміття. У прикладі WeakMap, як тільки всі інші посилання на об'єкт зникнуть - об'єкт можна видалити з WeakMap. Якщо ви все ще не впевнені, що я маю на увазі, будь ласка, повідомте мене
Бенджамін Грюнбаум

@ Бенджамін, нам потрібно розрізняти потребу в чутливому до пам’яті кеші та необхідності в пакеті даних_об'єкта. Не плутайте ці дві окремі вимоги. Ваш 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) заміна / розширення класу обіцянок з 1 або 2 або 3. або 6) заміна / розширення класу обіцянок з набором необхідних членів. - У будь-якому випадку, слабка карта не потрібна, якщо вам не потрібен кеш-пам'ять.
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);
    }
}

2
Я не зовсім розумію, як ви це використаєте. Це може спричинити крах спостережуваного разом із подіями, пов'язаними з ним, коли на них більше не посилаються. Проблема, на яку я схильна, полягає в тому, що спостерігач більше не посилається. Я думаю, що рішення тут вирішило лише половину проблеми. Я не думаю, що ви можете вирішити проблему спостерігача за допомогою WeakMap, оскільки це не під силу.
jgmjgm

1
Слухачі подій з подвійним буферизацією можуть бути швидкими іншими мовами, але в цьому випадку це просто езотерика і повільність. Це мої три копійки.
Джек Гіффін

@axelduch, Ого, цей міф про обробку слухачів був розроблений аж до спільноти Javascript, набравши 40 оновлень! Щоб зрозуміти , чому ця відповідь зовсім неправильно , див коментарі під stackoverflow.com/a/156618/632951
Pacerier

1
@Pacerier оновив відповідь, дякую за відгук
axelduch

1
@axelduch, так, теж є відгук.
Pacerier

18

WeakMap добре працює для капсулювання та приховування інформації

WeakMapдоступний лише для ES6 та вище. A WeakMap- це сукупність пар ключів і значень, де ключ повинен бути об'єктом. У наступному прикладі ми побудуємо a 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 (намагатися звільнити пам'ять до запобігти збою веб-переглядача) означає повільніше перегляд веб-переглядачів, а іноді і збій браузера.

Що стосується поліфілів для цих, то я б рекомендував власну бібліотеку ( знайдена тут @ github ). Це дуже легка бібліотека, яка просто поліфікує її без будь-якої із занадто складних рамок, які ви можете знайти в інших поліфілах.

~ Щасливе кодування!


1
Дякую за чітке пояснення. Приклад вартий більше, ніж будь-які слова.
newguy

@lolzery, Re " Це не дозволяє елементам DOM збирати сміття ", все, що вам потрібно, це встановити elementsна нуль, і ви все зробите: це буде GCed. & Re " Посилання на DOM, які відскакують над усім документом ", зовсім не має значення. Після того, elementsяк відійде основна посилання , весь круговий посилання буде GCed. Якщо ваш елемент тримається за посиланнями на елемент, який йому не потрібен, то виправте код і встановіть посилання на нуль, коли закінчите з його використанням. Це буде GCed. Слабкі карти не потрібні .
Pacerier

2
@Pacerier подякувати Вам за захоплену зворотний зв'язок, однак установка elementsна нуль буде НЕ дозволити браузер GC елементів в першій ситуації сніппети. Це відбувається тому, що ви встановлюєте власні властивості для елементів, а потім ці елементи все одно можна отримати, і до їхніх власних властивостей все ще можна отримати доступ, тим самим запобігаючи GC'ed жодному з них. Подумайте про це як про ланцюжок з металевих кілець. Доки ви маєте доступ до принаймні однієї ланки ланцюга, ви можете утримувати її за ланцюг і, таким чином, не допускати потрапляння цілого ланцюга предметів у прірву.
Джек Гіффін

1
виробничий код з дандером на ім’я vars змушує мене блювоти
Barbu Barbu

10

Я використовую WeakMapдля кешу безтурботного запам'ятовування функцій, які приймають незмінні об'єкти як їх параметр.

Пам'ять - це вигадливий спосіб сказати "після того, як ви обчислите значення, кешуйте його, щоб вам не довелося обчислювати його знову".

Ось приклад:

Кілька речей, які слід зазначити:

  • Об'єкти Immutable.js повертають нові об’єкти (з новим вказівником), коли ви їх модифікуєте, тому використання їх як ключів у WeakMap гарантує таке ж обчислене значення.
  • WeakMap чудово підходить для нагадувань, тому що, як тільки об'єкт (який використовується як ключ) збирається сміття, так і обчислюється значення на WeakMap.

1
Це допустиме використання слабкої карти до тих пір, поки кеш пам'яті повинен бути чутливим до пам'яті , не стійким протягом усього життя obj / function. Якщо "кеш пам'яті" має бути стійким протягом усього життя obj / function, тоді слабка карта є неправильним вибором: використовуйте будь-який із 6 методів інкапсуляції JavaScript за замовчуванням .
Pacerier

3

У мене є цей простий варіант використання / приклад використання для слабких карт.

УПРАВЛІННЯ КОЛЕКЦІєю КОРИСТУВАЧІВ

Я почав з 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, як приклад я додаю чотирьох користувачів та роздруковую їх: Додавання та друк користувачів

ДОДАТИ БІЛЬШЕ ІНФОРМАЦІЇ ДЛЯ КОРИСТУВАЧІВ БЕЗ МОДИФІКУВАННЯ Існуючого КОДУ

Тепер скажіть, що потрібна нова функція, в якій потрібно відстежувати посилання кожного платника соціальних медіа (SMP) разом із об'єктами користувача.

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

Це можливо з 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 (за допомогою збирання сміття), оскільки без User Object немає можливості отримати доступ до жодного його SMP-посилання.

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

Видалення користувальницького Білла з карти також видаляє посилання SMP

Немає вимоги будь-якого додаткового коду окремо видаляти SMP-посилання окремо, а існуючий код до того, як ця функція жодним чином не була змінена.

Якщо є якийсь інший спосіб додати цю функцію за допомогою / без слабких карт, будь ласка, коментуйте.


_____nice______
Алекс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.