Menu

Map i Set w JavaScript: kiedy używać ich zamiast obiektów i tablic

Jak działają Map i Set w JavaScript, czym różnią się od zwykłych obiektów i tablic oraz kiedy warto po nie sięgnąć.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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] i obj["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.size działa w O(1). Dla obiektu napiszesz Object.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), a array.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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ