Dwie kolekcje poza obiektem i tablicą
Zwykłe obiekty i tablice pokrywają większość potrzeb programów w JavaScript, ale nie zostały zaprojektowane do każdego zadania. Map i Set to wbudowane kolekcje, które wypełniają dwie konkretne luki: wyszukiwanie po kluczach, które nie są stringami, oraz sprawdzanie przynależności bez duplikatów.
Są w języku od ES2015. Obie są iterowalne, obie mają właściwość .size i obie dobrze współpracują z operatorem spread. Model myślowy jest prosty:
Map: jak obiekt, ale kluczem może być cokolwiek, a kolejność jest zachowana.Set: jak tablica, ale wartości są unikalne, a wyszukiwanie jest szybkie.
Tworzenie i używanie Map
Map przechowuje pary klucz/wartość. Tworzysz ją przez new Map() i używasz .set(), .get(), .has() oraz .delete():
Możesz też przekazać do konstruktora tablicę par [key, value], żeby od razu ją wypełnić:
Ten kształt dwuelementowej tablicy pojawia się wszędzie tam, gdzie w grę wchodzą mapy: tak właśnie reprezentowane są wpisy podczas iteracji.
Map kontra obiekt: po co to wszystko?
Zwykłe obiekty wyglądają, jakby robiły to samo. Zwykle rzeczywiście tak jest. Ale mapy naprawiają kilka konkretnych niedogodności:
Obiekty dziedziczą po Object.prototype, więc klucze takie jak toString, constructor i hasOwnProperty już istnieją w każdym obiekcie. Mapy nie mają takiego bagażu: istnieją tylko klucze, które sam ustawisz.
Inne różnice, które warto znać:
- Dowolny typ klucza. Mapy przyjmują jako klucze obiekty, funkcje, liczby i wartości logiczne. Obiekty po cichu zamieniają klucze niebędące stringami na stringi:
obj[1]iobj["1"]to to samo miejsce. - Gwarantowana kolejność dodawania. Mapy iterują w kolejności dodawania wpisów. Obiekty w większości też, ale klucze wyglądające jak liczby są sortowane na początek, co jest subtelną pułapką.
- Wbudowany rozmiar.
map.sizedziała w O(1). Dla obiektu napiszeszObject.keys(obj).length, co za każdym razem buduje tablicę. - Optymalizacja pod częste zmiany. Silniki dostrajają mapy pod częste dodawanie i usuwanie. Obiekty są dostrojone pod rekordy o stałym kształcie.
Używaj obiektu, gdy modelujesz rekord ze znanymi kluczami tekstowymi ({ name, email, age }). Używaj Map, gdy klucze są dynamiczne, nie są stringami albo gdy będziesz często dodawać i usuwać wpisy.
Iteracja po Map
Mapy są iterowalne, więc for...of działa bezpośrednio, a destrukturyzacja każdego wpisu jest naturalna:
Jeśli chcesz tylko klucze albo tylko wartości, wywołaj .keys() lub .values(). Jest też .forEach(), jeśli wolisz:
Żeby zamienić mapę z powrotem na zwykły obiekt lub tablicę, rozwiń ją:
Tworzenie i używanie Set
Set przechowuje unikalne wartości. Dodanie wartości, która już w nim jest, nic nie robi:
Unikalność ustala się tą samą zasadą równości co ===, z jednym wyjątkiem: w zbiorze NaN jest uznawany za równy samemu sobie, mimo że wszędzie indziej NaN === NaN daje false.
Przekaż do konstruktora obiekt iterowalny, żeby od razu wypełnić zbiór. Stąd bierze się trik na usuwanie duplikatów:
Jedna linia, dowolny typ prosty. Dla tablic obiektów to nie działa, bo dwa różne obiekty z tymi samymi polami to nadal dwie różne wartości. Ale dla stringów, liczb i wartości logicznych to idiomatyczny sposób na usuwanie duplikatów.
Set kontra tablica: kiedy się przesiąść
Tablice i zbiory przechowują kolekcję wartości, więc kiedy wybrać które?
Sięgnij po Set, gdy:
- Wartości muszą być unikalne i chcesz, żeby pilnowało tego środowisko uruchomieniowe.
- Często sprawdzasz przynależność.
set.has(x)działa w O(1), aarray.includes(x)w O(n). W pętli ta różnica szybko rośnie. - Wystarczy ci kolejność dodawania. Zbiory iterują w kolejności dodawania, ale nie obsługują indeksowania.
Zostań przy tablicy, gdy:
- Potrzebujesz dostępu po pozycji:
arr[0], wycinanie, sortowanie. - Duplikaty mają znaczenie: koszyk z dwiema sztukami tego samego produktu.
- Intensywnie używasz metod tablicowych takich jak
.map,.filter,.reduce. Zbiory ich nie mają, najpierw trzeba by je rozwinąć do tablicy.
Szybki przykład pod kątem wydajności:
Gdyby banned było tablicą, każdy callback filter przeszukiwałby całą listę. Jako Set każde wyszukiwanie działa w stałym czasie.
Iteracja po Set
Tak samo jak z Map: for...of po prostu działa, a rozwinięcie daje tablicę:
Zbiory udostępniają też .keys(), .values() i .entries() dla symetrii z Map, choć w zbiorze klucze i wartości to to samo. Najczęściej będziesz po prostu iterować bezpośrednio.
Przykład w praktyce: liczenie unikalnych odwiedzających na stronę
Połączenie obu kolekcji: Map ścieżek stron do zbiorów identyfikatorów odwiedzających:
Map obsługuje przypisanie ścieżki do koszyka, a Set usuwa duplikaty wewnątrz każdego koszyka. Próba zrobienia tego samego zwykłym obiektem i tablicami by zadziałała, ale na każdym kroku pisałbyś dodatkowe sprawdzenia indexOf i zabezpieczenia hasOwnProperty.
WeakMap i WeakSet w skrócie
Istnieją dwie powiązane kolekcje do wąskiego zastosowania: WeakMap i WeakSet. Przechowują referencje słabo, czyli wpis, którego klucz (w WeakMap) lub wartość (w WeakSet) nie ma innych referencji, jest automatycznie usuwany przez garbage collector.
Przyjmują jako klucze tylko obiekty, nie są iterowalne i nie mają .size. To celowe: gdyby dało się po nich iterować, działanie garbage collectora byłoby widoczne. Przydają się do przechowywania metadanych o obiektach, które nie są twoje, i rzadko pojawiają się w codziennym kodzie.
Dalej: JSON
Map i Set świetnie sprawdzają się w pamięci, ale żadna z nich nie przetrwa JSON.stringify w nienaruszonym stanie: mapy stają się {} i zbiory też stają się {}. Następna strona omawia JSON: jak serializować i parsować dane oraz jakie wzorce stosować, gdy kolekcje z tej strony muszą przejść przez sieć albo trafić do pliku.
Najczęściej zadawane pytania
Czym różni się Map od obiektu w JavaScript?
Map może używać dowolnej wartości jako klucza: obiektów, funkcji, liczb, czegokolwiek. Obiekt zamienia klucze na stringi (lub symbole). Map śledzi też swój rozmiar przez .size, iteruje w kolejności dodawania i nie dziedziczy kluczy z prototypu, więc nie ma ryzyka kolizji z toString czy constructor. Sięgnij po Map, gdy klucze nie są stringami albo gdy często dodajesz i usuwasz wpisy.
Do czego służy Set w JavaScript?
Set przechowuje unikalne wartości i po cichu ignoruje duplikaty. Najszybszy sposób na usunięcie duplikatów z tablicy to [...new Set(arr)]. Zbiory dają też sprawdzenia .has() w czasie O(1), co wygrywa z array.includes(), gdy sprawdzasz przynależność w pętli.
Jak iterować po Map?
for...of działa bezpośrednio: for (const [key, value] of myMap) rozkłada każdy wpis przez destrukturyzację. Możesz też iterować po myMap.keys(), myMap.values() lub myMap.entries(). Kolejność iteracji zawsze odpowiada kolejności dodawania, czego zwykłe obiekty nie zawsze gwarantują dla kluczy wyglądających jak liczby.