Menu

Symbol w JavaScript: unikalne klucze, Symbol.iterator i well-known symbols

Czym są symbole w JavaScript, po co istnieją i jak well-known symbols, takie jak Symbol.iterator, pozwalają twoim obiektom korzystać z funkcji języka.

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

Symbol to wartość o gwarantowanej unikalności

Symbol to jeden z typów prymitywnych JavaScriptu, obok string, number, boolean, null, undefined i bigint. Jego najważniejsza cecha jest prosta: każdy utworzony symbol różni się od każdego innego symbolu, na zawsze.

Dwa symbole, oba utworzone bez argumentów, a mimo to nie są równe. To nie przypadek, tylko cały sens symboli. Nie da się podrobić symbolu, przypadkiem odtworzyć go ani zderzyć się z czyimś symbolem.

Możesz dodać opis, który pomaga przy debugowaniu. Nie wpływa on na tożsamość:

Ten sam opis, różne symbole. Opis to wyłącznie etykieta dla ludzi czytających logi.

Po co istnieją: klucze bez kolizji

Główny powód istnienia symboli to możliwość dodawania właściwości do obiektu bez obawy, że twój klucz zderzy się z cudzym. Klucze w postaci stringów rywalizują w jednej płaskiej przestrzeni nazw: jeśli dwie biblioteki postanowią trzymać metadane w obj.meta, nadpiszą sobie nawzajem dane. Klucze symboliczne nie rywalizują, bo nikt inny nie ma twojego symbolu.

Nawiasy kwadratowe w [ID]: 42 to składnia obliczanej właściwości (computed property): oznacza „użyj wartości ID jako klucza”. Tylko kod, który ma ten sam symbol ID, może odczytać lub nadpisać tę właściwość. Każdy inny moduł, który używa stringa "id" albo własnego Symbol("id"), mówi o zupełnie innym miejscu w obiekcie.

Klucze symboliczne są (w większości) ukryte

Właściwości z kluczami symbolicznymi nie pojawiają się w for...in, Object.keys ani JSON.stringify. Nie są tajne, bo zdeterminowany kod nadal je znajdzie, ale nie przeszkadzają przy zwykłej iteracji.

Object.keys i JSON.stringify całkowicie pomijają klucz symboliczny. Jeśli chcesz celowo znaleźć właściwości symboliczne, udostępniają je Object.getOwnPropertySymbols i Reflect.ownKeys. To dokładnie takie zachowanie, jakiego chcesz dla metadanych: widoczne, gdy ich szukasz, niewidoczne dla kodu, który tylko przechodzi po kluczach w postaci stringów.

Współdzielenie symboli przez Symbol.for

Symbol() daje lokalny, jednorazowy symbol. Czasem potrzebujesz czegoś odwrotnego: symbolu, który jest taki sam w całym programie, we wszystkich modułach, żeby różne fragmenty kodu mogły uzgodnić klucz. Do tego służy Symbol.for.

Symbol.for(key) sprawdza globalny rejestr: jeśli symbol dla tego klucza już istnieje, dostajesz go z powrotem, a jeśli nie, zostaje utworzony i zapisany. Symbol.keyFor działa w drugą stronę: zwraca klucz, z którym utworzono zarejestrowany symbol (albo undefined dla niezarejestrowanych).

Najczęściej wystarczy zwykłe Symbol(). Po Symbol.for sięgaj wtedy, gdy symbol naprawdę musi być współdzielony ponad granicami modułów.

Well-known symbols: punkty zaczepienia w języku

Tu symbole naprawdę pokazują swoją wartość. JavaScript udostępnia zestaw predefiniowanych symboli, tak zwanych well-known symbols, których sam język szuka w twoich obiektach. Zaimplementuj metodę pod jednym z tych kluczy, a twój obiekt od razu współpracuje z wbudowaną funkcją języka.

Najbardziej przydatny jest Symbol.iterator. Każdy obiekt, który ma metodę pod tym kluczem, jest iterowalny: działa z for...of, spreadem, destrukturyzacją i Array.from.

range nie jest tablicą. To zwykły obiekt z jedną metodą pod specjalnym kluczem. Ponieważ jednak ta metoda zwraca iterator, for...of i spread działają na nim tak samo jak na tablicach, stringach i Map. Taka jest umowa: zaimplementuj Symbol.iterator, a język potraktuje twój obiekt jako iterowalny.

Inne well-known symbols, które warto znać

Jest ich jeszcze kilka, a każdy to punkt zaczepienia w innej części języka:

  • Symbol.iterator: sprawia, że obiekt jest iterowalny.
  • Symbol.asyncIterator: to samo, ale dla for await...of.
  • Symbol.toPrimitive: określa, jak obiekt zamienia się na wartość prymitywną (number, string albo default) w kontekstach koercji.
  • Symbol.hasInstance: zmienia to, co zwraca instanceof dla twojej klasy.
  • Symbol.toStringTag: ustawia etykietę pokazywaną przez Object.prototype.toString.call(obj).

Nie musisz ich zapamiętywać. Wystarczy wiedzieć, że istnieją: gdy chcesz, żeby twój obiekt zachowywał się jak typ wbudowany, zwykle jest do tego jakiś well-known symbol.

Symbole to nie stringi

Częsta pułapka: symbole nie zamieniają się automatycznie na stringi. Konkatenacja rzuca błąd, podobnie jak przekazanie symbolu do API, które oczekuje stringa:

Template literals rzucają ten sam TypeError. Gdy naprawdę chcesz tekst, użyj String(symbol) albo symbol.toString(). Te ostre krawędzie są celowe: język chroni cię przed przypadkowym potraktowaniem unikalnej wartości tożsamości jak kolejnego kawałka tekstu.

Kiedy używać symboli (a kiedy nie)

Sięgaj po symbole, gdy:

  • Dodajesz metadane do obiektów, które nie są twoje, i potrzebujesz klucza bez ryzyka kolizji.
  • Projektujesz protokół: „obiekty, które chcą współpracować z moją biblioteką, powinny mieć metodę pod tym symbolem”.
  • Chcesz właściwości, która nie trafi do JSON.stringify ani for...in.
  • Implementujesz Symbol.iterator albo inny well-known symbol.

Zrezygnuj z symboli, gdy:

  • Potrzebujesz po prostu klucza obiektu i nie ma ryzyka kolizji. String jest prostszy i w logach wygląda jak on sam.
  • Chcesz pól „prywatnych”. Właściwości z kluczami symbolicznymi nie są naprawdę prywatne, bo Object.getOwnPropertySymbols je znajdzie. Do prawdziwej prywatności używaj pól klasy #private.
  • Przechowujesz dane, które muszą przetrwać JSON.stringify. Nie przetrwają.

Większość kodu JavaScript świetnie radzi sobie bez bezpośredniego pisania Symbol(...). Ale gdy tylko chcesz, żeby twój obiekt działał z for...of albo naturalnie zachowywał się przy koercji, symbole są drzwiami do tej maszynerii.

Dalej: deklaracje funkcji

Symbole, iteratory i generatory mocno opierają się na funkcjach: metodach zapisanych pod Symbol.iterator, fabrykach zwracających iteratory, generatorach, których forma function* ukrywa większość szablonowego kodu. Funkcje to kolejny rozdział, zaczynając od różnych sposobów ich deklarowania i tego, co zmienia się między tymi formami.

Najczęściej zadawane pytania

Czym jest symbol w JavaScript?

Symbol to typ prymitywny, którego wartości mają gwarantowaną unikalność. Tworzysz go przez Symbol() albo Symbol('opis'), a żadne dwa wywołania nie dadzą równych symboli. Używa się ich głównie jako kluczy obiektów, które nie zderzą się z innymi kluczami, oraz jako punktów zaczepienia dla funkcji języka przez well-known symbols, takie jak Symbol.iterator.

Kiedy użyć symbolu zamiast klucza w postaci stringa?

Użyj symbolu, gdy chcesz dodać do obiektu właściwość bez ryzyka konfliktu nazw z innym kodem: biblioteki dodające metadane, frameworki oznaczające obiekty albo klucz, który celowo nie należy do publicznego API. Przy zwykłych kluczach, gdzie liczy się czytelność, a kolizje nie grożą, string nadal jest dobrym wyborem.

Do czego służy Symbol.iterator?

Symbol.iterator to well-known symbol, który mówi for...of, spreadowi i destrukturyzacji, jak iterować po obiekcie. Jeśli zdefiniujesz pod kluczem Symbol.iterator metodę zwracającą iterator, twój obiekt staje się iterowalny. Tak właśnie działają pod spodem tablice, stringi, Map i Set.

Czym różni się Symbol() od Symbol.for()?

Symbol('x') za każdym razem tworzy zupełnie nowy symbol: dwa wywołania z tym samym opisem i tak dają różne symbole. Symbol.for('x') sprawdza globalny rejestr i dla tego samego klucza zwraca ten sam symbol w całym programie. Używaj Symbol.for, gdy symbole mają być współdzielone między modułami lub realmami, a Symbol() do lokalnej unikalności.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ