Map i Set
Map i Set to dwie struktury danych, które są czymś pomiędzy tablicami i klasycznymi obiektami.
Map()
Mapy służą do tworzenia zbiorów z parami [klucz - wartość]. Przypominają one klasyczne obiekty (czy np. mapy z SASS), natomiast główną różnicą odróżniającą je od klasycznych obiektów, jest to, że kluczami może być tutaj dowolny typ danych.
Aby stworzyć mapę możemy skorzystać z jednej z 2 konstrukcji:
const map = new Map();
map.set("kolor1", "red");
map.set("kolor2", "blue");
//lub
const map = new Map([
["kolor1", "red"],
["kolor2", "blue"],
]);
Dla każdej mapy mamy dostęp do kilku metod:
set(key, value) | Ustawia nowy klucz z daną wartością |
---|---|
get(key) | Zwraca wartość danego klucza |
has(key) | Sprawdza czy mapa ma dany klucz |
delete(key) | Usuwa dany klucz i zwraca true/false jeżeli operacja się udała |
clear() | Usuwa wszystkie elementy z mapy |
entries() | Zwraca iterator zawierający tablicę par [klucz-wartość] |
keys() | Zwraca iterator zawierający listę kluczy z danej mapy |
values() | Zwraca iterator zawierający listę wartości z danej mapy |
forEach | robi pętlę po elementach mapy |
prototype[@@iterator]() | Zwraca iterator zawierający tablicę par [klucz-wartość] |
const map = new Map();
map.set("kolor1", "red");
map.set("kolor2", "blue");
map.set("kolor3", "yellow");
console.log(map.get("kolor1")); //red
console.log(map.delete("kolor2"));
console.log(map.keys()); //MapIterator {"kolor1", "kolor3"}
Aby pobrać długość mapy, użyjemy właściwości size:
const map = new Map();
map.set("kolor1", "red");
map.set("kolor2", "blue");
console.log(map.size); //2
console.log(map.length); //undefined
Klucze w mapie
Mapy w przeciwieństwie do obiektów mogą mieć klucze dowolnego typu, gdzie w przypadku obiektów (w tym tablic) są one konwertowane na tekst:
const map = new Map();
map.set("1", "Kot");
map.set(1, "Pies");
console.log(map); //{"1" => "Kot", 1 => "Pies"}
const ob = {}
ob["1"] = "Kot";
ob[1] = "Pies";
console.log(ob); //{"1" : "Pies"}
const map = new Map();
const ob1 = { name : "test1" }
const ob2 = { name : "test2" }
map.set(ob1, "koty");
map.set(ob2, "psy");
map.set("[object Object]", "świnki");
console.log(map); //{{…} => "koty", {…} => "psy", "[object Object]" => "świnki"}
W przypadku klasycznych obiektów, klucze zawsze są konwertowane na tekst (obiekty na zapis [object Object]
:
const map = {}
const ob1 = { name : "test1" }
const ob2 = { name : "test2" }
map[ob1] = "koty";
map[ob2] = "psy"; //ob2 skonwertowany na "[object Object]"
map["[object Object]"] = "świnki";
console.log(map); //{"[object Object]": "świnki"}
Pętla po mapie
Jeżeli będziemy chcieli iterować po mapie, możemy wykorzystać pętlę for of
i poniższe funkcje zwracające iteratory:
entries() | Zwraca tablicę par klucz-wartość |
---|---|
keys() | Zwraca tablicę kluczy |
values() | Zwraca tablicę wartości |
const map = new Map([
["kolor1", "red"],
["kolor2", "blue"],
["kolor3", "yellow"]
]);
for (const key of map.keys()) {
//kolor1, kolor2, kolor3
}
for (const key of map.values()) {
//red, blue, yellow
}
for (const entry of map.entries()) {
//["kolo1", "red"]...
}
for (const [key, value] of map.entries()) {
//key : "kolo1", value : "red"...
}
for (const entry of map) {
//["kolor1", "red"]...
}
Do iterowania możemy też wykorzystać wbudowaną w mapy funkcję forEach
:
const map = new Map([
["kolor1", "red"],
["kolor2", "blue"],
["kolor3", "yellow"]
]);
map.forEach((value, key, map) => {
console.log(`
Wartość: ${value}
Klucz: ${key}
`);
});
Set()
Obiekt Set jest kolekcją składającą się z unikalnych wartości, gdzie każda wartość może być zarówno typu prostego jak i złożonego. W przeciwieństwie do mapy jest to zbiór pojedynczych wartości.
Żeby stworzyć Set możemy użyć jednej z 2 składni:
const set = new Set();
set.add(1);
set.add("text");
set.add({name: "kot"});
console.log(set); //{1, "text", {name : "kot"}}
//lub
//const set = new Set(elementIterowalny);
const set = new Set([1, 1, 2, 2, 3, 4]); //{1, 2, 3, 4}
const set = new Set("kajak"); //{"k", "a", "j"}
Obiekty Set mają podobne właściwości i metody co obiekty typu Map, z małymi różnicami:
add(value) | Dodaje nową unikatową wartość. Zwraca Set |
---|---|
clear() | Usuwa wszystkie elementy ze zbioru |
delete(key) | Usuwa dany klucz i zwraca true/false jeżeli operacja się udała |
entries() | Zwraca iterator zawierający tablicę par [klucz-wartość] |
has(key) | Sprawdza czy mapa ma dany klucz |
keys() | Zwraca iterator zawierający listę kluczy z danej mapy |
values() | Zwraca iterator zawierający listę wartości z danej mapy |
forEach | robi pętlę po elementach mapy |
prototype[@@iterator]() | Zwraca iterator zawierający tablicę par [klucz-wartość] |
const mySet = new Set();
mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add("text"); //Set { 1, 5, "text"}
mySet.has(5); // true
mySet.delete(5); //Set {1, "text"}
console.log(mySet.size); //2
W przypadku Set() klucze i wartości są takie same, dlatego robiąc pętle nie ważne czy użyjemy powyższych values(), keys(), entries() czy po prostu zrobimy pętlę for of:
const set = new Set([1, "kot", "pies", "świnka"]);
//wszystkie pętle zadziałają podobnie
for (const val of set.values()) {
console.log(val);
}
for (const key of set.keys()) {
console.log(key);
}
for (const [key, val] of set.entries()) {
console.log(key, val); //key === val
}
for (const el of set) {
console.log(el);
}
Set i tablice
Dzięki temu, że Set zawiera niepowtarzające się wartości, możemy to wykorzystać do odsiewania duplikatów w praktycznie dowolnym elemencie iteracyjnym - np. w tablicy:
const tab = [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 5];
const set = new Set(tab);
console.log(set); //{1, 2, 3, 4, 5}
const uniqueTab = [...set];
console.log(uniqueTab); //[1, 2, 3, 4, 5]
const tab = [
"ala",
"bala",
"cala",
"ala",
"ala"
]
const tabUnique = [...new Set(tab)];
console.log(tabUnique); //["ala", "bala", "cala"]
To samo tyczy się oczywiście dynamicznie tworzonych setów:
const set = new Set("kot");
console.log(set) //Set {"k", "o", "t"}
set.add("k");
set.add("k");
set.add("t");
set.add("y");
console.log(set); //Set {"k", "o", "t", "y"}
WeakMap()
WeakMap to odmiana Mapy, którą od Map rozróżniają trzy rzeczy:
- Nie można po niej iterować (w przyszłości będzie można, bo już zapowiedziano odpowiednie zmiany)
- Kluczami mogą być tylko obiekty
- Jej elementy są automatycznie usuwane gdy do danego obiektu (klucza) nie będzie referencji
Aby stworzyć nową WeakMap, skorzystamy z instrukcji:
const ob1 = {};
const ob2 = {};
const ob3 = {};
const weak = new WeakMap();
weak.set(ob1, "lorem");
wm.set(ob2, {name : "Karol"});
weak.get(ob1); //"lorem"
weak.has(ob1); //true
weak.has(ob3); //false
Każda mapa daje nam kilka metod:
set(key, value) | Ustawia wartość dla klucza |
---|---|
get(key) | Pobiera wartość klucza |
has(key) | Zwraca true/false w zależności czy dana WeakMap posiada klucz o danej nazwie |
delete(key) | Usuwa wartość przypisaną do klucza |
W odróżnieniu do Map elementy WeakMap są automatycznie usuwane jeżeli do danego obiektu/klucza nie będzie żadnych referencji.
Co to oznacza? W rozdziale o Garbage Collection dyskutowaliśmy o zarządzaniu pamięcią i tym, że jeżeli na dany obiekt wskazuje jakakolwiek referencja, nie może on być usunięty z pamięci. Spójrzmy jeszcze raz na tamten przykład:
let ob = {
name : "Karol"
}
const tab = [ob];
ob = null;
console.log(tab[0]); //{name : "Karol"}
Jak widzisz, mimo, że zmienna ob
została ustawiona na null, obiekt nie został usunięty z pamięci, ponieważ wciąż wskazuje na niego pierwszy indeks tablicy tab[0]
. Podobna sytuacja będzie w przypadku Map:
let ob = { name : "Karol" }
const map = new Map();
map.set("user", ob);
ob = null;
map.get("user"); //{name : "Karol"}
Co zresztą nie jest dziwne. W przypadku WeakMap taki element zostanie automatycznie z niej usunięty:
let ob = { name : "Karol" }
const weak = new WeakMap();
weak.set(ob, "...");
ob = null;
console.log(weak);
Dzięki czemu obiekt będzie mógł być usunięty z pamięci, bo nie będzie na niego wskazywać żadna referencja.
Uwaga. Powyższe console.log pokaże nam w debuggerze WeakMap z elementem, którego nie powinno być. Wynika to z tego, że przy czyszczeniu pamięci działają pewne mechanizmy optymalizacyjne, które sprawiają, że nie musi on być odpalany przy każdej linijce kodu. Stąd w debuggerze nie zobaczysz wyniku usunięcia nieużytecznych obiektów.
Żeby realnie sprawdzić działanie takiego kodu, trzeba by wymusić odpalenie w danym momencie Garbage Collectora. Można to zrobić na kilka sposobów.
let ob = { name : "Karol" }
const weak = new WeakMap();
weak.set(ob, "...");
ob = null;
console.log(weak); //WeakMap({…} => "...")
gc(); //wymuszam czyszczenie pamięci w Chrome - ale trzeba to wcześniej włączyć!
console.log(weak); //WeakMap {}
Gdzie to może się przydać? W zasadzie wszędzie, gdzie będziemy przeprowadzać dodatkowe operacje na obiektach, które potencjalnie mogą być zaraz usunięte. Wyobraźmy sobie dla przykładu, że w jednym ze skryptów mamy naście obiektów reprezentujących pliki.
let file1 = { name: "file1", ext: "jpg" }
let file2 = { name: "file2", ext: "png" }
let file3 = { name: "file3", ext: "gif" }
Chcielibyśmy teraz napisać funkcję, która będzie do zmiennej readCount
zbierać informacje o liczbie przeczytań danego pliku:
const readCount = new Map();
function readFile(file) {
if (readCount.has(file)) {
const count = readCount.get(file) + 1;
readCount.set(file, count);
} else {
readCount.set(file, 1);
}
}
readFile(file1);
readFile(file1);
readFile(file1);
readFile(file2);
readFile(file3);
Wynik w zmiennej readCount
wyszedł nam jak należy.
console.log(readCount); //{{...file1...} => 3, {...file2...} => 1, {...file3...} => 1}
Problem z powyższym kodem jest ten sam co opisywany wcześniej. Na takich plikach mogą być przeprowadzane różne operacje. Może jakiś fragment kodu usunie dany plik? W takiej sytuacji nie powinniśmy już trzymać jego danych.
file2 = null;
file3 = null;
console.log(readCount); //{{...file1...} => 3, {...file2...} => 1, {...file3...} => 1}
Niestety w przypadku Map (ale i tablic czy obiektów) referencje będą trzymane do czasu, aż ich ręcznie z nich nie usuniemy. A to oznacza, że do powyższego kodu powinniśmy dorobić funkcjonalność, która usuwała by dane o nieistniejącym już pliku. Powiedzmy, że nie wiemy gdzie, albo nie możemy modyfikować fragmentu, który usuwa dane pliki. Jak sprawić by powyższy zbiór readCount
automatycznie się aktualizował?
I tutaj właśnie pojawia się zaleta WeakMap, w przypadku których odpowiednie wpisy będą automatycznie usuwane, gdy dany obiekt zostanie gdzieś usunięty i nie będzie już referencji do niego:
const readCount = new WeakMap();
function readFile(file) {
if (readCount.has(file)) {
const count = readCount.get(file) + 1;
readCount.set(file, count);
} else {
readCount.set(file, 1);
}
}
readFile(file1);
readFile(file1);
readFile(file1);
readFile(file2);
readFile(file3);
console.log(readCount); //{{...file1...} => 3, {...file2...} => 1, {...file3...} => 1}
file2 = null;
file3 = null;
console.log(readCount); //{{...file1...} => 3}
WeakSet()
Podobnie jak dla Map istnieją WeakMap, tak dla Setów istnieją WeakSet
. Są to kolekcje składające się z unikalnych obiektów. Podobnie do WeakMap obiekty takie będą automatycznie usuwane z WeakSet, jeżeli do danego obiektu zostaną usunięte wszystkie referencje.
const set = new WeakSet();
const a = {};
const b = {};
set.add(a);
set.add(b);
set.add(b);
console.log(set); //{a, b}
Każdy WeakSet udostępnia nam metody:
add(ob) | Dodaje dany obiekt do kolekcji |
---|---|
delete(ob) | Usuwa dany obiekt z kolekcji |
has(ob) | Zwraca true/false w zależności, czy dana kolekcja zawiera dany obiekt |
WeakSet idealnie nadaje się do zbierania w jeden zbiór obiektów, które potencjalnie w dalszej części skryptu mogą zostać usunięte, a więc nie powinny być trzymane w naszej "liście":
let user1 = {}
let user2 = {}
let user3 = {}
const userList = new WeakSet();
userList.add(user1);
userList.add(user2);
userList.add(user3);
user1 = null;
userList.has(user1); //false