Sets и Maps в ECMAScript 5

В ECMAScript 5 и по-рано, програмистите имитират sets и maps чрез използване на обектни свойства, като това:

let set = Object.create(null);

set.foo = true;

// проверка за съществуване
if (set.foo) {

    // направи нещо
}	
			

Променливата set в този пример е обект с прототип null, което гарантира, че няма наследени свойства на обекта. Това е често срещан подход в ECMAScript 5, който използва обектни свойства, като уникални стойности, които трябва да бъдат проверени. Свойствата се добавят към set обекта и се определят на true така, че лесно да могат да се използват в условни конструкции (като, if изявлението в този пример), за да може да се провери дали стойността е налична.

Единствената разлика между обект използван, като set и обект използван, като map е начина на съхранение на стойността. Например, този пример използва един обект като map:

let map = Object.create(null);

map.foo = "bar";

// извличане на стойност
let value = map.foo;

console.log(value);         // "bar"
			

Този код съхранява string стойност "bar" под ключ "foo". За разлика от sets, maps се използват предимно за извличане на информация, а не само за проверка за съществуване на ключа.

Проблеми със заобикаляне

Докато подхода за използване на обекти, като sets и maps работи добре в прости ситуации, той може да стане по-сложен, когато се сблъска с ограниченията на обектни свойства. Тъй като, всички обектни свойства трябва да са strings, ние трябва да сме сигурни, че няма два ключа, които да оценяват същия string. Помислете за следното:

let map = Object.create(null);

map[5] = "foo";

console.log(map["5"]);      // "foo"
			

Този пример присвоява стойността на string "foo" към цифров ключ 5. Вътрешно, цифровата стойност се превръща в string, така map["5"] и map[5] всъщност са референция към едно и също свойство. Това вътрешно преобразуване може да предизвика проблеми, когато искаме използваме числа и strings, като ключове. Друг проблем е, когато се използват обекти, като ключове:

let map = Object.create(null),
    key1 = {},
    key2 = {};

map[key1] = "foo";

console.log(map[key2]);     // "foo"
			

Тука, map[key2] указва една и същата стойност, като map[key1]. Обектите key1 и key2 се преобразуват в strings, защото обектните свойства трябва да са strings. Тъй като, "[object Object]" е string представянето по подразбиране за обекти, така key1 и key2 се преобразуват в string. Това може да доведе до грешки, които да не са очевидни, тъй като е логично да се предположи, че различните ключове обекти в действителност са различни.

Преобразуването на представянето в string по подразбиране, прави трудно използването на обекти, като ключове. (Същият проблем съществува, когато се опитваме да използваме един обект, като set.)

Друг проблем се отнася конкретно до maps с ключ, чиято стойност е falsy. Стойността falsy автоматично се превръща във false, когато се използва в ситуации, при които се изисква булева стойност, като условие за if изявление. Това само по себе си не е проблем, стига да сте внимателни затова как използвате стойностите. Например:

let map = Object.create(null);

map.count = 1;

// проверка за наличие на "count" или не нулева стойност
if (map.count) {
    // ...
}
			

Този код има известна неяснота по отношение на използването на map.count. Предназначението на if изявлението е да провери за наличието на map.count или че стойността не е нулева. Кода вътре в if изявлението ще се изпълни защото стойността 1 е истина. Обаче, ако map.count е 0 или map.count не съществува, кодът вътре в if изявлението няма да бъде изпълнен.

Това е труден проблем за идентифициране и отстраняване на грешки, когато това се случи в големи приложения, което е и основна причина поради която ECMAScript 6 добавя sets и maps в езика.

info
JavaScript има in оператор, който връща true, ако свойството съществува в обекта, без да чете стойността на обекта. Освен това, in операторът също търси в прототипа на един обект, което го прави безопасен за използване, когато един обект е с null прототип. Дори и така, много програмисти все още неправилно използват код, като в последния пример, а не използват in.

Sets в ECMAScript 6

ECMAScript 6 добавя Set тип, който е подреден списък от стойности без дубликати. Sets позволява бърз достъп до данните, съдържащи се в рамката му, добавяйки по-ефективен начин за проследяване на дискретни стойности.

Създаване на Sets и добавяне на елементи

Set се създава с помощта на new Set() и елементи се добавят към set използвайки add() метод. Можем да видим, колко елемента са в set използвайки size свойство.

let set = new Set();
set.add(5);
set.add("5");

console.log(set.size);    // 2
			

Sets не коригира стойностите за определяне на типа, те са едни и същи. Това означава, един set може да съдържа, както номер 5 така и string "5", като два отделни елемента. (Вътрешно сравнението се прави с помощта на Object.is() метода, който е обсъден в Глава 4, за да определи дали двете стойности са едни и същи). Можете да добавите множество обекти, към set и те ще останат различни:

let set = new Set(),
    key1 = {},
    key2 = {};

set.add(key1);
set.add(key2);

console.log(set.size);    // 2
			

Тъй като, key1 и key2 не се преобразуват в strings, те се броят за два уникални елемента в set. (Не забравяйте, че ако те се превръщат в strings и двата ще са равни на "[Object object]".)

Ако add() метода се извика повече от веднъж с една и съща стойност, всички извиквания след първото се игнорират ефективно:

let set = new Set();
set.add(5);
set.add("5");
set.add(5);     // дублирането се игнорира

console.log(set.size);    // 2
			

Можете да инициализирате set използвайки масив и Set конструктора ще гарантира, че се използват само уникални стойности:

let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(set.size);    // 5
			

В този пример, масив с дублиращи се стойности се използва за инициализиране на set. Броя 5 се появява само веднъж в set въпреки, че изглежда четири пъти в масива. Тази функционалност прави лесно преобразуването на съществуващ код или JSON структури с използването на sets.

info
Set конструктора всъщност приема всеки iterable обект, като аргумент. Масивите работят, защото те са iterable по подразбиране, също като sets и maps. Set конструктора използва итератор за извличане на стойности от аргумента.(Iterables и итераторите са разгледани в Глава 8.)

Можете да тествате за да видите, кои стойности са в set използвайки has() метод:

let set = new Set();
set.add(5);
set.add("5");

console.log(set.has(5));    // true
console.log(set.has(6));    // false
			

Тук set.has(6) ще се върне false, защото set няма такава стойност.

Премахване на стойност

Възможно е да се премахват стойности от set. Можете да премахнете една стойност с помощта на delete() метода или да премахнете всички стойности от set с помощта на clear() метода. Този код показва това в действие:

let set = new Set();
set.add(5);
set.add("5");

console.log(set.has(5));    // true

set.delete(5);

console.log(set.has(5));    // false
console.log(set.size);      // 1

set.clear();

console.log(set.has("5"));  // false
console.log(set.size);      // 0
			

След извикването на delete(), само 5 го няма; след изпълнението на clear() метода, set е празен.

Всичко това е само един много лесен механизъм за проследяване на уникални подредени стойности. Обаче, ако искате да добавите елемент в set и след това да изпълните някоя операция върху всеки елемент, е момента, в който метода forEach() идва на помощ.

Метода forEach() за Sets

Ако сте свикнали да използвате масиви тогава вече може да сте запознати с forEach() метода. ECMAScript 5 добавя forEach() към масиви, за да се създаде по-лесен начин за работа върху всеки елемент в масив, без създаване на for цикъл. Метода се оказа популярен сред програмистите и така един и същи метод е наличен и за sets и работи по същия начин.

Метода forEach() се подава на функция за обратно извикване и приема три аргумента.

  1. Стойността от следващата позиция в set.
  2. Същата стойност, като първия аргумент.
  3. Set от който чете стойността.

Странна разлика между set версията на forEach() и версията за масив, е че първия и втория аргумент на функцията за обратно извикване са едни и същи. Въпреки, че това изглежда, като грешка има добра причина затова поведение.

Другите обекти, които имат forEach() метод (масиви и maps) подават три аргумента към техните функции за обратно извикване. Първите два аргумента за масиви и maps са стойност и ключ (цифров индекс за масиви).

Sets нямат ключове, обаче. Хората зад стандарта на ECMAScript 6 биха могли да направят функцията за обратно извикване да приема два аргумента, което ще я направи различна от другите две. Вместо това те намерили начин да запазят функцията за обратно извикване да приема три аргумента и така sets разглежда всяка стойност едновременно, като ключ и стойност. Като такива, първият и вторият аргумент са винаги едни и същи в forEach() за sets, за да се запази същата функционалност в съответствие с другите forEach() методи за масиви и maps.

Освен разликата с аргументите, използването на forEach() е основно същото, както за set така и за масиви. Ето част от код, който показва начина на работа:

let set = new Set([1, 2]);

set.forEach(function(value, key, ownerSet) {
    console.log(key + " " + value);
    console.log(ownerSet === set);
});
			

Този код минава над всеки елемент във set и на изхода извежда стойностите подадени към forEach() функцията за обратно извикване. Всеки път, когато функцията обратно извикване се изпълнява, key и value са еднакви и ownerSetе винаги e равно на set. Това е изхода:

				
1 1
true
2 2
true

Също, като при масиви, можете да подадете this стойност, като втори аргумент на forEach(), ако имате нужда да използвате this в вашата функция за обратно извикване:

let set = new Set([1, 2]),
    processor = {
        output(value) {
            console.log(value);
        },
        process(dataSet) {
            dataSet.forEach(function(value) {
                this.output(value);
            }, this);
        }
    };

processor.process(set);
	    	

В този пример, метода processor.process() извиква forEach() върху set и подава this, като this стойност за обратно извикване. Това е необходимо, така че this.output() правилно да реши по отношение на processor.output() метода. forEach() функцията използва само първия аргумент value, така че другите са пропуснати. Може да използвате функция стрела, за да получите същия ефект, без да подавате втори аргумент:

let set = new Set([1, 2]),
    processor = {
        output(value) {
            console.log(value);
        },
        process(dataSet) {
            dataSet.forEach((value) => this.output(value));
        }
    };

processor.process(set);
	    	

Функцията стрела в този пример, чете this от съдържащата process() функция и така правилно решава this.output() по отношение на processor.output() извикването.

Имайте в предвид, че sets са чудесни стойности за проследяване и forEach() ви позволява да работите върху всяка стойност последователно, но не можете да получите директен достъп до индекса на стойност, както можете в масив. Ако трябва да направите това, тогава най-добрия вариант е да превърнете set в масив.

Превръщане на Set в масив

Превръщането на масив в set е лесно, защото можем да подадем масива на Set конструктора. Също така е лесно да превърнем set обратно в масив, използвайки оператора spread. Оператора spread (...) беше обсъден в Глава 3, като начин да се разделят елементите в масив на отделни функционални параметри. Можем също да използваме оператора spread върху iterable обекти, също като sets, който да ги превръща в масив. Например:

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
    array = [...set];

console.log(array);             // [1,2,3,4,5]
	    	

Тука, set е зареден първоначално с масив, който съдържа дубликати. Set премахва дубликатите и след това елементите се поставят в нов масив, използвайки оператора spread. Самия set все още съдържа същите елементи (1,2,3,4 и 5), които е получил, когато е бил създаден. Те само са копирани в нов масив.

Този подход е полезен, когато вече имаме масив и искаме да създадем масив без дубликати. Например:

function eliminateDuplicates(items) {
    return [...new Set(items)];
}

let numbers = [1, 2, 3, 3, 3, 4, 5],
    noDuplicates = eliminateDuplicates(numbers);

console.log(noDuplicates);      // [1,2,3,4,5]
	    	

Във функцията eliminateDuplicates(), set е само временен посредник, използван за филтриране на дублирани стойности преди създаването на нов масив, който няма дубликати.

Weak Sets

Set типа алтернативно може да се нарече силен комплект, заради начина по който съхранява референции към обекти. Един обект се съхранява в инстанция на Set ефективно по същия начин, както се съхранява този обект в променлива. Докато препратка към тази инстанция на Set съществува, обектът не може да бъде почистен от събрания garbage (боклук) за да се освободи памет. Например:

let set = new Set(),
    key = {};

set.add(key);
console.log(set.size);      // 1

// премахване на оригиналната референция
key = null;

console.log(set.size);      // 1

// вземане на оригиналната референция обратно
key = [...set][0];
	    	

В този пример, key се настройва на null и изчиства една препратка към key обекта, но друга остава вътре в set. Все още можете да извлечете key, чрез превръщане на set в масив, използвайки оператора spread и достъп до първия елемент. Това работи добре за повечето програми, но понякога е по-добре референциите в set да изчезнат, когато и всички други референции изчезнат. Например, ако вашия JavaScript код се изпълнява в уеб-страница и искате да следите DOM елементите, които могат да бъдат премахнати от друг скрипт, вие не искате вашия код да държи последната референция към DOM елемента - (това се нарича загуба на памет).

За да облекчи тези въпроси, ECMAScript 6 включва Weak sets, в които съхранява само нетрайни референции към обекти и не могат да съхраняват примитивни стойности. Нетрайна референция към обект е тази, която не възпрепятства garbage collector (събирача на боклук), ако това е единствената останала препратка.

Създаване на WeakSet

Weak sets се създават с помощта на WeakSet конструктора и имат add(), has() и delete() методи. Ето един пример, който използва всичките три:

let set = new WeakSet(),
    key = {};

// добавяне на обект към set
set.add(key);

console.log(set.has(key));      // true

set.delete(key);

console.log(set.has(key));      // false
	    	

Използването на weak set прилича на използването на редовен set. Можете да добавяте, премахвате и проверявате в weak set. Можете също така да изпращате weak set със стойности подадени, като iterable към конструктора:

let key1 = {},
    key2 = {},
    set = new WeakSet([key1, key2]);

console.log(set.has(key1));     // true
console.log(set.has(key2));     // true
	    	

В този пример, масива се подава на WeakSet конструктора. Тъй като, този масив съдържа два обекта, тези обекти са добавени в weak set. Имайте в предвид, че ще бъде хвърлена грешка, ако масивът съдържа някакви стойности, които не са обект, тъй като WeakSet не може да приема примитивни стойности

Основни разлики между Set типовете

Най-голямата разлика между weak sets и редовен sets е нетрайната референция, държана към стойността на обекта. Ето един пример, който показва разликата:

let set = new WeakSet(),
    key = {};

// добавяне на обект към set
set.add(key);

console.log(set.has(key));      // true

// премахване на последната силна референция към ключа, като също я премахва и от weak set
key = null;
	    	

След изпълнението на този код, референцията към key в weak set вече не е достъпна. Не е възможно да се провери неговото премахване, защото ще трябва една препратка към този обект да се подаде към has() метода. Това може да направи тестването на weak sets малко объркващо, но вие може да се доверите, че препратката е била правилно отстранена от JavaScript машината.

От тези примери, може да се види, че weak sets споделя някои характеристики с редовния sets, но има някои основни разлики. Те са:

  1. В инстаницята на Weak sets, методите add(), has() и delete() хвърлят грешка, когато им се подава не-обект.
  2. Weak set не са iterables и следователно не могат да се използват в for-of цикъл.
  3. Weak sets не излагат никакви итератори (като keys() и values() методите), така че няма начин програмно да се определи съдържанието на weak set.
  4. Weak sets не разполагат с forEach() метод.
  5. Weak sets не разполагат с size свойство.

Привидно ограничената функционалност на weak sets е необходима за правилно справяне с паметта. По принцип, ако имате нужда само да следите препратки към обекти, тогава трябва да използвате weak set вместо редовния set.

Sets дават нов начин да се справим със списъци от стойности, но те не са полезни, когато трябва да се асоциира допълнителна информация с тези стойности. Ето защо, ECMAScript 6 добавя maps.

Maps в ECMAScript 6

ECMAScript 6 Map типа е подреден списък от двойки ключ-стойност, където ключа и стойността могат да бъдат от всякакъв тип. Ключовата равностойност се определя с помощта на Object.is(), така че може да имате ключ от 5 и ключ от "5", нищо, че са различни видове. Това е доста по различно, отколкото при използване на обектни свойства, като ключове, които винаги коригираме в string стойности.

Елементи се добавят към maps с помощта на set() метод и подаване на ключ и стойност за асоцииране с ключа. След това можем да извлечем стойност чрез подаване на ключа към get() метода. Например:

let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);

console.log(map.get("title"));      // "Understanding ES6"
console.log(map.get("year"));       // 2016
	    	

В този пример, се съхраняват две двойки ключ-стойност. Ключа "title" съхранява string, докато ключа "year" съхранява номер. По-късно, get() метода извлича стойностите от двата ключа. Ако ключа не съществува в map, get() връща специалната стойност undefined.

Можете също да използвате обекти, като ключове, нещо, което е невъзможно при използване на обектни свойства. Ето един пример:

let map = new Map(),
    key1 = {},
    key2 = {};

map.set(key1, 5);
map.set(key2, 42);

console.log(map.get(key1));         // 5
console.log(map.get(key2));         // 42	    		
	    	

Този код използва обектите key1 и key2, като ключове в map за съхраняване на две различни стойности. Тъй като тези ключове не са коригирани в друга форма, всеки обект се счита за уникален. Това ви позволява да свържете допълнителни данни към даден обект, без да променяте самия обект.

Map методи

Maps споделят няколко метода с sets. Така умишлено ви се дава възможност да взаимодействате с maps и sets по подобни начини. Има три метода на разположение за maps и sets:

  1. has(key) - показва дали даден ключ съществува в map
  2. delete(key) - изтрива ключ и свързаната с него стойност от map
  3. clear() - изтрива всички ключове и стойности от map

В допълнение, maps имат size свойство, което показва, колко двойки ключ-стойности съдържа. Този код използва трите метода и size по различни начини:

let map = new Map();
map.set("name", "Nicholas");
map.set("age", 25);

console.log(map.size);          // 2

console.log(map.has("name"));   // true
console.log(map.get("name"));   // "Nicholas"

console.log(map.has("age"));    // true
console.log(map.get("age"));    // 25

map.delete("name");
console.log(map.has("name"));   // false
console.log(map.get("name"));   // undefined
console.log(map.size);          // 1

map.clear();
console.log(map.has("name"));   // false
console.log(map.get("name"));   // undefined
console.log(map.has("age"));    // false
console.log(map.get("age"));    // undefined
console.log(map.size);          // 0
	    	

Както при sets, size свойството винаги съдържа броя на двойките ключ-стойност в map. Този пример започва с два ключа "name" и "age", така че has() връща true, когато му е подаден ключ. След това ключа "name" се отстранява с помощта на delete() метода, така че, has() връща false, когато му е подадено "name" и size свойството показва един елемент по-малко. Метода clear() след това премахва останалите ключове и has() връща false за всички ключове и size е 0.

Метода clear() е бърз начин за премахване на голямо количество данни от map, но има и начин за добавяне на много данни в map на един път.

Map инициализация

Също като sets, можем да инициализираме map с подаване на масив от данни към Map конструктора. Всеки елемент в масива, трябва да бъде сам по себе си масив, където първия елемент е ключът, а втория съответната стойност. Целия map, следователно е масив от тези два под-масива, например:

let map = new Map([ ["name", "Nicholas"], ["age", 25]]);

console.log(map.has("name"));   // true
console.log(map.get("name"));   // "Nicholas"
console.log(map.has("age"));    // true
console.log(map.get("age"));    // 25
console.log(map.size);          // 2
	    	

Ключовете "name" и "age" се добавят в map чрез инициализация в конструктора. Понеже масива от масиви изглежда малко странно, не е необходимо да си представяме точно ключове, тъй като те могат да бъдат всякакъв тип данни. Съхраняването на тези ключове в масив е единствения начин да се гарантира, че те не са коригирани в друг тип данни, преди да бъдат съхранени в map.

Метода forEach за Maps

Метода forEach() за maps е подобен на forEach() за sets и масиви, който приема функция за обратно извикване, която получава три аргумента:

  1. Стойността от следващата позиция в map
  2. Ключът за тази стойност
  3. Map от който се чете стойността

Аргументите за обратно извикване по-точно съответстват на forEach() поведението за масиви, където първия аргумент е стойността, а втория е ключът (съответстващ на цифров индекс в масива). Ето един пример:

let map = new Map([ ["name", "Nicholas"], ["age", 25]]);

map.forEach(function(value, key, ownerMap) {
    console.log(key + " " + value);
    console.log(ownerMap === map);
});	
	    	

Функцията за обратно извикване на forEach() извежда информацията, която се предава в нея, value и key се извеждат директно, a ownerMap се сравнява с map за да покаже, че стойностите са еквивалентни. Това е изхода:

	    		

name Nicholas
true
age 25
true

Функцията за обратно извикване на forEach() получава всяка двойка ключ-стойност в реда на тяхното вмъкване в map. Това е малко по-различно от извикването на forEach() за масиви, където обратното извикване получава всеки елемент в реда на цифровия индекс.

info
Можете също така да представите втори аргумент на forEach() за да уточните this стойноста вътре във функцията за обратно извикване. Това поведение е същото, както при set версията на forEach() метода.

Weak Maps

Weak maps са maps, както weak sets са sets, което е начин за съхраняване на нетрайни референции към обект. В weak maps, всеки ключ трябва да бъде обект (и ще бъде хвърлена грешка ако се опитате да използвате ключ не-обект) и тези референции към обекти се събират нетрайно, така че да не са в противоречие с garbage collector. Когато няма други препратки към weak map ключа извън weak map, двойката ключ-стойност се изважда от weak map.

Най-полезното място за използване на weak maps е при създаването на обект, свързан с конкретен DOM елемент в уеб-страница. Например, някои JavaScript библиотеки за уеб-страници поддържат един потребителски обект за всеки DOM елемент, който е посочен в библиотеката и мапнат се съхранява вътрешно в кеша на обектите.

Най-трудната част от този подход е да се определи, кога DOM елемента вече не съществува в уеб-страницата, така че библиотеката да може да премахне свързания с нея обект. В противен случай, библиотеката ще задържи препратката на DOM елемента и по този начин ще предизвика загуба на памет. Проследяването на DOM елементи със weak map все пак позволява на библиотеката да свърже един потребителски обект с всеки DOM елемент и след това автоматично да унищожи този обект, когато DOM елемента не съществува вече.

info
Важно е да се отбележи, че само weak map ключове, а не weak map стойности, са weak референции. Ако един обект се съхранява, като weak map стойност, това ще попречи на garbage collector, ако всички други референции са премахнати.

Използване на Weak Maps

В ECMAScript 6 WeakMap типа е неподреден списък от двойки ключ-стойност, където ключа трябва да бъде не нулев обект, а стойността може да бъде всякакъв тип. Интерфейса за WeakMap е много подобен на този за Map в това, че set() и get() се използват за добавяне и извличане на данни съответно.

let map = new WeakMap(),
    element = document.querySelector(".element");

map.set(element, "Original");

let value = map.get(element);
console.log(value);             // "Original"

// премахване на елемент
element.parentNode.removeChild(element);
element = null;

// the weak map е празен в този момент
	    	

В този пример, се съхранява една двойка ключ-стойност. Ключът element е DOM елемент, който се използва за съхраняване на съответната string стойност. След това тази стойност се извлича чрез подаване на DOM елемента към get() метода. Когато DOM елемента след това се премахне от документа и променливата референция е настроена на null, тогава данните се отстраняват от weak map.

Подобно на weak sets, няма начин да се провери дали weak map е празен, тъй като няма size свойство. Понеже няма останали препратки към ключа, не можете да използвате get() метода да се опита да извлече стойността. Weak map е отрязал достъпа до стойността на този ключ и когато garbage collector заработи, паметта заета от стойността ще бъде освободена.

Инициализация на Weak Map

Weak maps могат да се инициализират по същия начин, като редовните maps: чрез подаване на масив от масиви към WeakMap конструктора. Точно като инициализацията на map, всеки елемент в масива трябва да бъде сам по себе си масив с два елемента, където първият елемент е ключът (не нулев обект) и втория елемент е стойността (всички типове данни). Например:

let key1 = {},
    key2 = {},
    map = new WeakMap([ [key1, "Hello"], [key2, 42]]);

console.log(map.has(key1));     // true
console.log(map.get(key1));     // "Hello"
console.log(map.has(key2));     // true
console.log(map.get(key2));     // 42
	    	

Обектите key1 и key2 се използват, като ключове в weak map и те могат да бъдат достъпни с get() и has() методите. Грешка се хвърля, ако конструктора на WeakMap получи ключ, който не е обект за някоя от двойките ключ-стойност.

Weak Map методи

Weak maps имат само два допълнителни метода на разположение, за да взаимодействат с двойките ключ-стойност. Това са has() метод за определяне дали съществува даден ключ в map и delete() метод за премахване на специфична двойка ключ-стойност. Няма clear() метод, защото това би изисквало изброяване на ключове и както при weak sets, това също не е възможно за weak maps. Този пример използва has() и delete() методи:

let map = new WeakMap(),
    element = document.querySelector(".element");

map.set(element, "Original");

console.log(map.has(element));   // true
console.log(map.get(element));   // "Original"

map.delete(element);
console.log(map.has(element));   // false
console.log(map.get(element));   // undefined
	    	

Тука, DOM елемента отново е използван, като ключ в weak map. Метода has() е подходящ за проверка, за да се види дали референцията в момента се използва, като ключ в weak map. Имайте в предвид, че това работи само, когато имате не-нулева референция на ключ. Ключът насилствено е премахнат от weak map с помощта на delete() метода, в която точка has() връща false и get() връща undefined.

Частни обект данни

Докато повечето програмисти обмислят основната употреба на случаи на weak maps, за асоцииране на данни с DOM елементи, има много други възможни употреби (и няма съмнение, че има някои, които все още предстоят да бъдат открити). Една практическа употреба на weak maps е да съхранява данни, които са частни инстанции на обект. Всички обектни свойства са публични в ECMAScript 6 и ще трябва да използвате някои творчески идеи, да направите данните достъпни за обекти, но не достъп до всичко. Да разгледаме следващия пример:

function Person(name) {
    this._name = name;
}

Person.prototype.getName = function() {
    return this._name;
};
	    	

Този код използва общата конвенция от водеща долна черта, за да покаже, че този обект се счита за частен и не трябва да бъде променян извън инстанцията на обекта. Намерението е да се използва getName() за да прочете this._name и да не позволи на тази стойност да бъде променяна. Обаче, няма нищо което да стои на пътя на някой да пише на _name свойството, така че да може да бъде презаписано умишлено или случайно.

Използвайки ECMAScript 5, беше възможно да се доближим до истински частни данни, чрез създаване на обект с помощта на модел, като този например:

let Person = (function() {

    let privateData = {},
        privateId = 0;

    function Person(name) {
        Object.defineProperty(this, "_id", { value: privateId++ });

        privateData[this._id] = {
            name: name
        };
    }

    Person.prototype.getName = function() {
        return privateData[this._id].name;
    };

    return Person;
}());	
	    	

Този пример увива дефиницията на Person с IIFE , която съдържа две частни променливи privateData и privateId. Обекта privateData съхранява лична информация за всеки отделен случай, докато unique ID се използва за генериране на уникално ID за всеки отделен случай. Когато Person конструктора се извика, се добавя свойството _id, така че да е nonenumerable, nonconfigurable и nonwritable.

След това, се прави запис в privateData обекта, който съответства на ID за инстанция на обект, това е когато name се съхранява. По-късно в getName() функцията, името може да бъде извлечено с помоща на this._id, като ключ в privateData. Понеже privateData не е достъпна извън IIFE, актуалните данни са в безопасност, въпреки че this._id е изложено публично.

Големият проблем при този подход е, че данните в privateData никога не изчезват, защото няма начин да се знае, кога е унищожена една обектна инстанция, което означава, че обекта privateData винаги ще съдържа допълнителни данни. Този проблем може да бъде решен, чрез използване на weak map:

let Person = (function() {

    let privateData = new WeakMap();

    function Person(name) {
        privateData.set(this, { name: name });
    }

    Person.prototype.getName = function() {
        return privateData.get(this).name;
    };

    return Person;
}());	
	    	

Тази версия на кода използва weak map за частни данни, вместо обект. Тъй като, самата инстанция на Person обектa може да се използва, като ключ, няма нужда да следите отделно ID. Когато Person конструктора се извика, се прави нов запис в weak map с ключ this и стойност на даден обект, който съдържа лична информация. В този случай, тази стойност е обект, съдържащ само name. Функцията getName() извлича тази лична информация, чрез подаване на this към privateData.get() метода, който извлича стойността на обекта и има достъп до свойството name. По този начин личната информация се пази в тайна и ще бъде унищожена, когато инстанцията на свързания с нея обект е унищожена.

Използване и ограничения на weak map

При вземането на решение, дали да използвате weak map или редовен map, основният мотив е дали искате да използвате само ключове обекти. Всеки път, когато вие ще използвате само ключове обекти най-добрия избор е weak map. Това ще ви позволи да оптимизирате използването на паметта и да избегнете загуба на памет, като гарантирате, че допълнителните данни не се съхраняват на около след, като вече не са достъпни.

Имайте в предвид, че weak maps дават много малка видимост в съдържанието им, така че не можете да използвате forEach(), size или clear() за управление на елементите. Ако имате нужда от някои възможности за инспекция, тогава редовния maps е по-добрия избор. Само не забравяйте да държите под око използването на паметта.

Разбира се, ако искате да използвате ключове не-обекти, тогава редовния maps е вашия избор.