Miniatura artykułu

Symbole w JavaScript

12 minut

Skopiuj link

Data publikacji: 10/1/2023, 12:03:05

Ostatnia aktualizacja: 4/1/2024

Czym jest symbol?

Jest to jeden z ośmiu typów, które mamy do dyspozycji w JavaScript - mowa tu o symbol pisanym małą literą. Mamy również Symbol zapisany wielką literą i jest to po prostu wbudowany w język obiekt służący między innymi do tworzenia kolejnych symboli (tych zapisywanych małą literą), podobna sytuacja występuje w przypadku większości typów prostych. Zatem zapis Symbol() zwraca nową wartość o typie symbol.

Mimo, że symbol jest typem prymitywnym (tak samo jak string, czy number), to jest porównywany za pomocą referencji (podobnie jak obiekty), więc mamy gwarancję, że każdy symbol jest unikatową, prymitywną wartością, która nigdy się nie powtórzy. Zatem próba porównania ze sobą dwóch symboli zawsze zwróci false.

Fakt, że symbole są zawsze unikatową wartością jest także ich głównym atutem i to właśnie z niego wynika ich najczęstsze zastosowanie.

Obiekt Symbol może przyjąć jeden argument - description. Wiele osób myśli, że jeżeli utworzymy dwa symbole używając tej samej wartości jako argumentu, to będą one sobie równe, dlatego już na wstępnie chciałbym obalić ten mit:

Argument ten wyląduje we właściwości description danego symbolu, a jego głównym zastosowaniem jest debugowanie:

Zastosowanie

Zdecydowanie najczęściej wykorzystuje się je do tworzenia ukrytych właściwości w obiekcie (według specyfikacji JavaScript tylko dwa typy mogą być użyte jako klucz obiektu: string oraz symbol). Takie podejście stosuje również sam język - wiele wbudowanych (o ile nie wszystkie) obiektów posiada właściwości utworzone z użyciem symboli.

Tak utworzone właściwości nie mogą również zostać przypadkowo nadpisane - żeby to zrobić wymagane jest użycie dokładnie tego samego symbolu, który został wykorzystany do ich utworzenia, zatem nie ma tu mowy o pomyłce, co może się zdarzyć w przypadku "zwykłych" kluczy.

Wspomniałem wcześniej, że właściwości utworzone z użyciem symbolu są ukryte, jednak nie jest to do końca prawda - istnieją sposoby na odczytanie ich, ale tak, jak w przypadku nadpisania, muszą zostać świadomie użyte. Jeżeli spróbujemy odczytać je za pomocą takich metod jak Object.keys(), Object.values(), Object.entries(), czy pętli for...in, to zostaną one pominięte.

Skoro "zwykłe" pętlę nie widzą takich właściwości, to jak możemy je odczytać?
Są dwa sposoby - pierwszy z nich to zastosowanie Object.getOwnPropertySymbols(). Metoda ta zwraca tablicę ze wszystkimi symbolami, które zostały użyte do utworzenia właściwości w danym obiekcie. Kolejny krok to po prostu użycie jednego z nich do odczytania interesującej nas właściwości.

Drugim (nieco mniej popularnym) sposobem jest zastosowanie metody ownKeys z obiektu Reflect. Różnica polega na tym, że ta metoda zwróci wszystkie właściwości, a nie tylko te, które są utworzone z użyciem symbolu i prawdopodobnie stąd wynika jej mniejsza popularność w przypadku tego zadania.

Oprócz tworzenia ukrytych właściwości symbole są także stosowane do modyfikowania zachowań obiektów poprzez zmianę implementacji wbudowanych właściwości. Jeżeli brzmi to zagadkowo, to nie przejmuj się, za chwilę wszystko się wyjaśni.

Musisz wiedzieć, że wiele obiektów w JavaScript posiada już ukryte właściwości utworzone z użyciem symboli, są one odpowiedzialne za zachowanie obiektu w różnych sytuacjach. Przykładem może tutaj być:

  • Czy na obiekcie można zastosować operator spread (...)

  • Czy obiekt jest iterowalny (np. z użyciem pętli)

  • Jak zachowa się obiekt podczas próby konwersji na typ prymitywny

My, jako programiści możemy je nadpisać i zmodyfikować, a tym samym zmienić domyślne zachowanie obiektu. Należy jednak pamiętać, że takie właściwości również mogą zostać skopiowane do innego obiektu (np. z użyciem Object.assign), co może umknąć uwadze i zarazem całkowicie zmienić działanie nowego obiektu.

Symbole w praktyce

Do tej pory utworzyliśmy kilka właściwości z użyciem symbolu, jednak zdecydowanie częściej wykorzystuje się je do nadpisania istniejących już właściwości, które dostarczyli nam twórcy języka.

Obiekt Symbol oprócz kilku metod (omówimy je w dalszej części) posiada także właściwości, które same są symbolami. To właśnie one zostały wykorzystane do utworzenia ukrytych przed światem właściwości w obiektach i dzięki nim możemy je także zmienić.

Najczęściej spotkasz je pod angielską nazwą well-known symbols. Dzisiaj poznamy kilka z nich.

Zacznijmy od Symbol.iterator - jest on odpowiedzialny za zachowanie obiektu w pętli for...of, lub w przypadku użycia operator spread.

Ponieważ generator zwraca dokładnie to, czego oczekuje pętla for...of, to jego zastosowanie jest tu idealnym przykładem. Oczywiście implementacja jest dowolna i nie musi to być ta z przykładu powyżej, tak naprawdę może być to dowolne zachowanie.

W przykładzie powyżej odtworzyłem zachowanie metody Object.entries(), ale ten sam efekt możemy osiągnąć bez użycia generatora (chociaż będzie to znacznie mniej czytelny kod i zdecydowanie nie polecam tego robić, a zamieszczony kod jest raczej ciekawostką):

Żeby zrozumieć działanie tego kawałka kodu trzeba poznać także zasadę działania pętli oraz generatorów, a to temat na kolejny artykuł. Potraktuj go jako ciekawostkę i jeżeli kiedyś zechcesz zaimplementować w obiekcie działanie, które przypomina pętlę, to koniecznie użyj w tym celu generatorów.

Omówmy jeszcze jeden z wbudowanych symboli. Symbol.toPrimitive jest nieco mniej przydatny, ale za to znacznie łatwiejszy do zrozumienia:

Otrzymuje on jeden argument, który zależy od tego, w jaki sposób został użyty dany obiekt i może przyjąć wartość "string", "number" lub "default". Na jego podstawie możemy zdefiniować co powinno się wtedy wydarzyć z obiektem.

Ponieważ użyłem obiektu w stringu, to parametr hint przyjął wartość "string", gdybym użył go w działaniu matematycznym, to byłby to "number", natomiast "default" występuje jedynie z powodów historycznych np. dla porównania person == 1.

Zanim przejdziemy dalej pokażę Ci jeszcze jeden przykład z zastosowaniem symbolu oraz pętli. Utworzymy obiekt range, który będzie iterowalny, a wartościami będą kolejne liczby.

Przykład ten ma na celu pokazanie, że za pomocą symboli można utworzyć niemal dowolne zachowanie i jak zwykle ograniczna nas tylko nasza wyobraźnia.

Oczywiście temat wbudowanych symboli nie jest wyczerpany, a dla każdego z nich implementacja będzie przebiegać nieco inaczej (w końcu zastosowanie też jest inne). Opis dwóch powyższych symboli miał na celu wprowadzenie do tego świata i nakreślenie ogólnego zastosowania.

Warto jest zapoznać się z dostępnymi możliwościami, ale zdecydowanie nie warto jest się uczyć sposobów implementacji na pamięć! Czasami wystarczy wiedzieć, jakie możliwości mamy dostępne i jedynie odświeżyć sobie pamięć.

Globalny Rejestr

Wspomniałem wcześniej, że każdy symbol jest unikalny i jest to prawda, ale możliwe jest również uzyskanie tego samego symbolu w różnych częściach aplikacji, jeżeli zostanie on zapisany w globalnym rejestrze symboli. Symbole w nim zapisane nazywane są globalnymi.

Symbol do globalnego rejestru możemy dodać za pomocą jednej z metod obiektu Symbol, a konkretnie Symbol.for("klucz").

Ta sama metoda służy również od odczytania globalnego symbolu. Jeżeli symbol o danym kluczu istnieje, to zostanie zwrócony, jeżeli nie, to zostanie utworzony.

Do dyspozycji mamy również inną metodę - Symbol.keyFor(symbol), która zamiast symbolu zwraca jego klucz w globalnym rejestrze. Argumentem w tym przypadku jest symbol, dla którego sprawdzamy klucz.

W ten sposób można odczytać tylko globalne symbole. Jeżeli dany symbol nie jest globalny (a więc nie został utworzony z użyciem metody Symbol.for()), nie możemy go odczytać za pomocą powyższych metod.

Jeżeli chcesz dać możliwość odczytania właściwości utworzonej za pomocą symbolu, to dodanie go do globalnego rejestru może okazać się bardzo pomocne.

Podsumowanie

Symbole to z pewnością jedna z bardziej zaawansowanych konstrukcji w JavaScript, a pełne poznanie i zrozumienie tematu może zająć trochę czasu.

Z ich pomocą możemy tworzyć niewidoczne właściwości w obiektach oraz modyfikować standardowe zachowania niektórych konstrukcji języka. Powinieneś jednak używać ich z rozwagą, ponieważ większość programistów jest po prostu przyzwyczajona do pewnego działania danych elementów języka i modyfikowanie ich może znacznie utrudnić im pracę, jeżeli się tego nie spodziewają.

Zwróć również szczególną uwagę na kopiowanie obiektów, w których symbole są wykorzystane do utworzenia właściwości - one również zostaną skopiowane!

Należy także pamiętać, że format JSON nie obsługuje tego typu danych, zatem zostaną one usunięte przy każdej próbie zamiany obiektu JavaScript na obiekt JSON.

Oczywiście temat nie jest wyczerpany, a jedynie powierzchownie omówiony, ale potrzebowałbym co najmniej jednego rozdziału książki, żeby w pełni go wyjaśnić. To, co powinieneś wynieść, to ogólna wiedza na temat obiektu Symbol, zastosowania symboli oraz umiejętność zajrzenia do dokumentacji w razie potrzeby 🙂

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy