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 в езика.
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 използвайки array и Set конструктора ще гарантира, че се използват само уникални стойности:
-
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]); console.log(set.size); // 5
В този пример, array с дублиращи се стойности се използва за инициализиране на set. Броя 5 се появява само веднъж в set въпреки, че изглежда четири пъти в array. Тази функционалност прави лесно преобразуването на съществуващ код или JSON структури с използването на sets.
Можете да тествате за да видите, кои стойности са в 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
Ако сте свикнали да използвате arrays тогава вече може да сте запознати с forEach() метода. ECMAScript 5 добавя forEach() към arrays, за да се създаде по-лесен начин за работа върху всеки елемент в array, без създаване на for цикъл. Метода се оказа популярен сред програмистите и така един и същи метод е наличен и за sets и работи по същия начин.
Метода forEach() се подава на функция за обратно извикване и приема три аргумента.
-
- Стойността от следващата позиция в set.
- Същата стойност, като първия аргумент.
- Set от който чете стойността.
Странна разлика между set версията на forEach() и версията за array, е че първия и втория аргумент на функцията за обратно извикване са едни и същи. Въпреки, че това изглежда, като грешка има добра причина затова поведение.
Другите обекти, които имат forEach() метод (arrays и maps) подават три аргумента към техните функции за обратно извикване. Първите два аргумента за arrays и maps са стойност и ключ (цифров индекс за arrays).
Sets нямат ключове, обаче. Хората зад стандарта на ECMAScript 6 биха могли да направят функцията за обратно извикване да приема два аргумента, което ще я направи различна от другите две. Вместо това те намерили начин да запазят функцията за обратно извикване да приема три аргумента и така sets разглежда всяка стойност едновременно, като ключ и стойност. Като такива, първият и вторият аргумент са винаги едни и същи в forEach() за sets, за да се запази същата функционалност в съответствие с другите forEach() методи за arrays и maps.
Освен разликата с аргументите, използването на forEach() е основно същото, както за set така и за arrays. Ето част от код, който показва начина на работа:
-
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
Също, като при arrays, можете да подадете 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() ви позволява да работите върху всяка стойност последователно, но не можете да получите директен достъп до индекса на стойност, както можете в array. Ако трябва да направите това, тогава най-добрия вариант е да превърнете set в array.
Превръщане на Set в array
Превръщането на array в set е лесно, защото можем да подадем array на Set конструктора. Също така е лесно да превърнем set обратно в array, използвайки оператора spread. Оператора spread (...) беше обсъден в Глава 3, като начин да се разделят елементите в array на отделни функционални параметри. Можем също да използваме оператора spread върху iterable обекти, също като sets, който да ги превръща в array. Например:
-
let set = new Set([1, 2, 3, 3, 3, 4, 5]), array = [...set]; console.log(array); // [1,2,3,4,5]
Тука, set е зареден първоначално с array, който съдържа дубликати. Set премахва дубликатите и след това елементите се поставят в нов array, използвайки оператора spread. Самия set все още съдържа същите елементи (1,2,3,4 и 5), които е получил, когато е бил създаден. Те само са копирани в нов array.
Този подход е полезен, когато вече имаме array и искаме да създадем array без дубликати. Например:
-
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 е само временен посредник, използван за филтриране на дублирани стойности преди създаването на нов array, който няма дубликати.
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 в array, използвайки оператора 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
В този пример, array се подава на WeakSet конструктора. Тъй като, този array съдържа два обекта, тези обекти са добавени в weak set. Имайте в предвид, че ще бъде хвърлена грешка, ако array съдържа някакви стойности, които не са обект, тъй като 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, но има някои основни разлики. Те са:
-
- В инстаницята на Weak sets, методите add(), has() и delete() хвърлят грешка, когато им се подава не-обект.
- Weak set не са iterables и следователно не могат да се използват в for-of цикъл.
- Weak sets не излагат никакви итератори (като keys() и values() методите), така че няма начин програмно да се определи съдържанието на weak set.
- Weak sets не разполагат с forEach() метод.
- 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:
-
- has(key) - показва дали даден ключ съществува в map
- delete(key) - изтрива ключ и свързаната с него стойност от map
- 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 с подаване на array от данни към Map конструктора. Всеки елемент в array, трябва да бъде сам по себе си array, където първия елемент е ключът, а втория съответната стойност. Целия map, следователно е array от тези два под-масива, например:
-
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 чрез инициализация в конструктора. Понеже array от arrays изглежда малко странно, не е необходимо да си представяме точно ключове, тъй като те могат да бъдат всякакъв тип данни. Съхраняването на тези ключове в array е единствения начин да се гарантира, че те не са коригирани в друг тип данни, преди да бъдат съхранени в map.
Метода forEach за Maps
Метода forEach() за maps е подобен на forEach() за sets и arrays, който приема функция за обратно извикване, която получава три аргумента:
-
- Стойността от следващата позиция в map
- Ключът за тази стойност
- Map от който се чете стойността
Аргументите за обратно извикване по-точно съответстват на forEach() поведението за arrays, където първия аргумент е стойността, а втория е ключът (съответстващ на цифров индекс в array). Ето един пример:
-
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() за arrays, където обратното извикване получава всеки елемент в реда на цифровия индекс.
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 елемента не съществува вече.
Използване на 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: чрез подаване на array от arrays към WeakMap конструктора. Точно като инициализацията на map, всеки елемент в array трябва да бъде сам по себе си array с два елемента, където първият елемент е ключът (не нулев обект) и втория елемент е стойността (всички типове данни). Например:
-
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.
Private обект данни
Докато повечето програмисти обмислят основната употреба на случаи на weak maps, за асоцииране на данни с DOM елементи, има много други възможни употреби (и няма съмнение, че има някои, които все още предстоят да бъдат открити). Една практическа употреба на weak maps е да съхранява данни, които са private инстанции на обект. Всички обектни свойства са публични в ECMAScript 6 и ще трябва да използвате някои творчески идеи, да направите данните достъпни за обекти, но не достъп до всичко. Да разгледаме следващия пример:
-
function Person(name) { this._name = name; } Person.prototype.getName = function() { return this._name; };
Този код използва общата конвенция от водеща долна черта, за да покаже, че този обект се счита за private и не трябва да бъде променян извън инстанцията на обекта. Намерението е да се използва getName() за да прочете this._name и да не позволи на тази стойност да бъде променяна. Обаче, няма нищо което да стои на пътя на някой да пише на _name свойството, така че да може да бъде презаписано умишлено или случайно.
Използвайки ECMAScript 5, беше възможно да се доближим до истински private данни, чрез създаване на обект с помощта на модел, като този например:
-
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 , която съдържа две private променливи 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 за private данни, вместо обект. Тъй като, самата инстанция на 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 е вашия избор.