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 debugerze 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 debugerze 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

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.