ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawiły się w 6. edycji standardu ECMAScript, w skrócie ES6.
Czym są symbole ES6?
Symbole to nie logo.
Nie są to malutkie obrazki, które możesz umieścić w kodzie.
let ? = ? × ?; // błąd składni
Symbol ES6 nie jest również środkiem stylistycznym mającym ukryte znaczenie.
Czym więc są symbole?
Siódmy typ
Od czasu ustandaryzowania JavaScriptu w 1997 r. w języku tym istniało sześć typów. Przed pojawieniem się standardu ES6 każdą wartość w programie JS można było zakwalifikować do którejś z poniższych kategorii:
undefined
null
- typ logiczny (boolean)
- liczba
- łańcuch
- obiekt
Każdy typ reprezentuje zbiór wartości. Pierwsza piątka to zbiory skończone. Istnieją oczywiście tylko dwie wartości logiczne, true
i false
, i innych już nie będzie. Więcej jest natomiast wartości liczbowych i łańcuchowych. Według standardu istnieje 18.437.736.874.454.810.627 różnych wartości liczbowych (łącznie z NaN
– to skrót od angielskiego wyrażenia Not a Number, oznaczający wartość niebędąca liczbą). To jednak nic w porównaniu z liczbą możliwych łańcuchów. Wydaje mi się, że to wartość rzędu (2144115188075855872 − 1) ÷ 65.535… chociaż mogłem pomylić się w obliczeniach.
Wartości obiektowe stanowią natomiast zbiór otwarty. Każdy obiekt jest unikalny niczym płatek śniegu. Przy każdym otwarciu dowolnej strony internetowej tworzona jest masa nowych obiektów.
Symbole ES6 są wartościami, lecz nie łańcuchami. Nie są to także obiekty. To coś nowego: siódmy typ wartości.
Pomówmy więc o sytuacji, w której mogłyby się nam przydać.
Jeden prosty typ logiczny
Czasem niezmiernie wygodnie byłoby móc przechować dodatkowe dane w obiekcie JavaScript należącym do kogoś innego.
Załóżmy na przykład, że piszesz bibliotekę JS, która wykorzystuje efekty przejścia CSS do poruszania elementów struktury DOM po ekranie. Zauważyłeś jednak, że nie można chyba zastosować jednocześnie kilku efektów przejścia na pojedynczym elemencie div
– powoduje to nieestetyczne, nieregularne przeskoki. Wydaje ci się, że da się to naprawić, ale najpierw trzeba w jakiś sposób sprawdzić, czy dany element już się porusza.
Jak rozwiązać taki problem?
Jednym sposobem jest wykorzystanie interfejsów API CSS, które wyślą do przeglądarki odpowiednie zapytanie. Byłaby to chyba jednak mała przesada. Biblioteka powinna już wiedzieć czy element się porusza – to jej kod wprawił go w ruch!
Zależy nam tak naprawdę na tym, by móc monitorować poruszające się elementy. Moglibyśmy przechowywać je w tablicy. Za każdym razem, gdy wywołamy bibliotekę w celu animowania elementu, będziemy mogli tablicę przeszukać i sprawdzić, czy dany element już się w niej znajduje.
Hmm. Jeśli tablica jest duża, przeszukiwanie liniowe będzie wolne.
Tak naprawdę wystarczy, by na elemencie ustawić flagę:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
Jednak i w tym przypadku możemy napotkać potencjalne problemy, a wszystko dlatego, że nasz kod nie korzysta ze struktury DOM jako jedyny.
- Na utworzoną przez nas własność może natrafić inny kod, wykorzystujący np. pętlę
for-in
lub metodęObject.keys()
. - Na pomysł wykorzystania tej techniki mógł wpaść już inny twórca biblioteki, przez co nasza biblioteka nie będzie z nią dobrze współpracować.
- Na to samo rozwiązanie może kiedyś wpaść jeszcze inny twórca, z którego biblioteką nasza też nie będzie działać prawidłowo.
- Komisja standaryzacyjna może postanowić o dodaniu metody
.isMoving()
do wszystkich elementów. To by był dopiero problem!
Oczywiście z ostatnich trzech sytuacji możemy wybrnąć, wybierając tak długi bądź niedorzeczny łańcuch, że nikomu innemu podobna nazwa nie wpadłaby do głowy:
if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;
Szkoda jednak psuć sobie wzrok.
Można wygenerować praktycznie unikatową nazwę własności z zastosowaniem technik kryptograficznych:
// wygeneruj 1024 bezsensowne znaki Unicode
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
Składnia obiekt[nazwa]
pozwala wykorzystać jako nazwę własności dosłownie dowolny łańcuch. Zatem sposób ten jest obiecujący: kolizje są praktycznie niemożliwe, a kod wygląda w porządku.
Pojawią się jednak problemy przy debugowaniu. Wynik każdorazowego wywołania metody console.log()
na elemencie o takiej własności będzie nieczytelny. Co jeśli będziesz potrzebować więcej takich własności? Jak je ujednolicić? Po każdym przeładowaniu ich nazwa będzie się zmieniać.
Dlaczego to takie trudne? Potrzebujemy tylko jednego typu logicznego!
Rozwiązaniem są symbole
Symbole to wartości, które programy mogą tworzyć i wykorzystywać jako klucze własności bez ryzyka kolizji nazw.
var mySymbol = Symbol();
Wywołanie metody Symbol()
powoduje utworzenie nowego symbolu, wartości wyjątkowej spośród wszystkich wartości.
Symbol może być kluczem własności podobnie jak łańcuch czy liczba. Ponieważ jednak symbole są inne od wszystkich łańcuchów, mamy pewność, że własność o kluczu symbolowym nie będzie kolidowała z żadną inną własnością.
obj[mySymbol] = "ok!"; // na pewno nie spowoduje kolizji
console.log(obj[mySymbol]); // ok!
Oto jak można zastosować symbole w opisanej powyżej sytuacji:
// utwórz unikatowy symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
Kilka uwag do powyższego kodu:
- Łańcuch
"isMoving"
w wyrażeniuSymbol("isMoving")
nazywamy opisem. Opisy przydają się w debugowaniu. Są wyświetlane podczas przesyłania symboli do konsoli za pomocą metodyconsole.log()
, przekształcania ich na typ łańcuchowy za pomocą metody.toString()
, a także mogą pojawić się w komunikatach o błędach. To wszystko. element[isMoving]
nazywamy własnością o kluczu symbolowym. Jest to po prostu własność, której nazwą nie jest łańcuch, lecz symbol. Poza tym nie różni się pod żadnym względem od innych zwykłych własności.- Podobnie jak w przypadku elementów tablicy, do własności o kluczu symbolowym nie można uzyskać dostępu poprzez składnię kropkową, np.
obiekt.nazwa
. W tym celu musimy skorzystać z nawiasu kwadratowego. - Jeśli natomiast mamy już nasz symbol, uzyskanie dostępu do własności o kluczu symbolowym to pestka. Powyższy przykład pokazuje jak pobrać i ustawić wartość
element[isMoving]
. Moglibyśmy także zapytać czy element się porusza za pomocą instrukcji warunkowejif (isMoving in element)
, bądź w razie potrzeby usunąć go za pomocą metodydelete element[isMoving]
. - Z drugiej strony, wszystkie te operacje możliwe są jedynie wtedy, gdy symbol
isMoving
znajduje się w zakresie. Z tego powodu symbole mogą posłużyć jako mechanizm słabej hermetyzacji: moduł tworzący kilka własnych symboli może użyć ich na dowolnym obiekcie, bez ryzyka kolizji z własnościami z innego kodu.
Ponieważ klucze symbolowe powstały z myślą o zapobieganiu kolizjom, są one ignorowane przez większość głównych narzędzi JavaScript do inspekcji obiektów. Przykładowo pętla for-in
przegląda tylko łańcuchowe klucze obiektu. Klucze symbole są pomijane. Podobnie jest w przypadku metod Object.keys(obj)
i Object.getOwnPropertyNames(obj)
. Symbole nie są jednak w zupełności prywatne: korzystając z nowego interfejsu API Object.getOwnPropertySymbols(obj)
możemy wygenerować listę kluczy symbolowych obiektu. Inne nowe API, Reflect.ownKeys(obj)
, zwraca zarówno klucze łańcuchowe jak i symbolowe. (API Reflect
omówimy wyczerpująco w kolejnym artykule).
Symbole znajdą szereg zastosowań w bibliotekach i systemach szkieletowych, lecz i sam język, jak się niebawem przekonamy, wykorzystuje je do wielu celów.
Czym jednak dokładnie są symbole
> typeof Symbol()
"symbol"
Symboli nie da się tak zupełnie porównać z niczym innym.
Po utworzeniu stają się one niemodyfikowalne. Nie można im nadać własności (w trybie ścisłym zostanie zwrócony błąd typu). Można je wykorzystać jako nazwy własności. To wszystko cechy typowe dla łańcuchów.
Z drugiej jednak strony każdy symbol jest niepowtarzalny, różny od wszystkich innych (nawet tych mających taki sam opis), a tworzenie nowych symboli nie sprawia kłopotów. To z kolei cechy charakteryzujące obiekty.
Symbole ES6 przypominają bardziej tradycyjne symbole z języków Lisp czy Ruby, lecz nie są one zintegrowane z językiem w tak dużym stopniu. W języku Lisp wszystkie identyfikatory to symbole, zaś w JS identyfikatory i większość kluczy własności traktowanych jest jako łańcuchy. Symbole są tylko dodatkową opcją.
Jedna uwaga: symboli, w przeciwieństwie do praktycznie wszystkich pozostałych elementów JS, nie można automatycznie przekształcić na łańcuchy. Próba połączenia symbolu z łańcuchem wywoła błąd typu.
> var sym = Symbol("<3");
> "twój symbol to " + sym
// TypeError: can't convert symbol to string
> `twój symbol to ${sym}`
// TypeError: can't convert symbol to string
Błędu tego można uniknąć, zamieniając symbol na łańcuch z zastosowaniem jawnej konwersji String(sym)
lub metody sym.toString()
.
Trzy zestawy symboli
Istnieją trzy sposoby na uzyskanie symbolu.
- Wywołanie metody
Symbol()
. Jak już powiedzieliśmy, metoda ta za każdym razem zwraca nowy, unikatowy symbol. - Wywołanie metody
Symbol.for(string)
. W ten sposób otrzymujemy dostęp do zbioru istniejących już symboli, tzw. rejestru symboli. W przeciwieństwie do unikatowych symboli definiowanych przez metodęSymbol()
, symbole w rejestrze są współdzielone. Jeśli więc wywołasz metodęSymbol.for("kot")
trzydzieści razy, to za każdym razem zostanie zwrócony ten sam symbol. Rejestr przydaje się, kiedy wiele stron internetowych lub wiele modułów jednej strony musi współdzielić jakiś symbol. - Skorzystanie z symboli zdefiniowanych w standardzie, np.
Symbol.iterator
. Kilka symboli jest zdefiniowanych w samym standardzie, a każdy z nich ma specjalną funkcję.
Jeśli wciąż nie jesteś przekonany czy symbole są rzeczywiście użyteczne, ostatnia kategoria jest ciekawa, ponieważ znajdziesz w niej autentyczne przykłady zastosowań symboli w praktyce.
Jak standard ES6 wykorzystuje znane symbole
Poznaliśmy już jeden sposób, w jaki ES6 korzysta z symboli – służą one uniknięciu konfliktów z istniejącym kodem. Jakiś czas temu, w artykule na temat iteratorów, zobaczyliśmy, że pętla for (var item of myArray)
rozpoczyna się wywołaniem metody myArray[Symbol.iterator]()
. Wspomniałem, że metoda ta mogłaby nosić nazwę myArray.iterator()
, jednak w celu zapewnienia zgodności wstecznej lepiej posłużyć się symbolem.
Łatwo to teraz zrozumieć skoro wiemy już, jak działają symbole.
Oto kilka innych zastosowań znanych symboli w ES6. (Poniższe elementy funkcjonalności nie zostały jeszcze zaimplementowane w Firefoksie).
- Możliwość rozszerzenia operatora
instanceof
. W ES6 wyrażenieobiekt instanceof konstruktor
zdefiniowane jest jako metoda konstruktora:konstruktor[Symbol.hasInstance](obiekt)
. Oznacza to, że jest ono rozszerzalne. - Wyeliminowanie konfliktów pomiędzy nowymi składnikami a starym kodem. Przyczyny tego zjawiska są bardzo niejasne, jednak ustaliliśmy że sama obecność niektórych metod tablicowych ES6 na istniejących stronach internetowych powoduje ich awarię. W innych standardach sieciowych występowały podobne problemy: wystarczyło dodać w przeglądarce nowe metody, by istniejące strony uległy awarii. W głównej mierze było to jednak spowodowane tzw. dynamicznym określaniem zakresu. W ES6 wprowadzono więc specjalny symbol
Symbol.unscopables
, który może być wykorzystywany przez standardy sieciowe, by niektóre metody nie były brane pod uwagę w dynamicznym określaniu zakresu. - Obsługiwanie nowych rodzajów dopasowywania łańcuchów. W ES5 metoda
str.match(mojObiekt)
od razu próbowała przekonwertowaćmojObiekt
na obiektRegExp
, zaś w ES6 najpierw sprawdza, czymojObiekt
ma metodęmojObiekt[Symbol.match](str)
. Obecnie, za pośrednictwem bibliotek, można korzystać z własnych klas parsujących łańcuchy, które działają wszędzie tam, gdzie działają obiektyRegExp
.
Każde z wymienionych zastosowań jest dosyć wąskie i nie wydaje mi się, by którykolwiek z tych elementów funkcjonalności miał znaczący wpływ na pisany przeze mnie na co dzień kod. W szerszej perspektywie są one jednak bardziej interesujące. Znane symbole w JS to udoskonalona wersja __podwojnegoPodkreslenia
z PHP i Pythona. W przyszłości staną się one częścią standardu, by umożliwić dodawanie do języka nowych punktów zaczepienia bez zakłócania działania istniejącego kodu.
Kiedy będę mógł zacząć korzystać z symboli ES6?
Symbole są zaimplementowane w 36. wersji Firefoksa i 38. wersji przeglądarki Chrome. W Firefoksie zaimplementowałem je ja, zatem wiesz kogo szukać w razie kłopotów.
By móc korzystać z symboli w przeglądarkach, które nie obsługują standardowo tych składników ES6, możesz użyć wypełniacza, np. core.js. Ponieważ symbole nie przypominają w stu procentach żadnego istniejącego już elementu funkcjonalności JS, wypełniacz ten nie jest idealny. Informacje dotyczące jego ograniczeń znajdują się tutaj.
W następnej odsłonie serii przedstawimy dwa nowe artykuły. Na początek omówimy kilka długo wyczekiwanych składników, które wreszcie staną się częścią JavaScriptu w ES6 – i trochę sobie na nie ponarzekamy. Zaczniemy od dwóch elementów, których historia sięga niemalże samych początków programowania. Następnie powiemy o dwóch bardzo podobnych składnikach, które jednak oparte są na efemerach (ang. ephemerons). Odwiedzaj nas zatem często, by szczegółowo poznać kolekcje w ES6.
Czarna magia
Heh, czyli taki GUID w JS? Choć GUID nie jest do końca unikalny… Ale w ramach jednej aplikacji JS Symbol jest zapewne w pełni unikalny 🙂
Fajne, Dzięki!