Symbole

Typ danych symbol to prymitywny typ danych, który zawsze zawiera unikalną wartość. Nie wiemy ile ona wynosi, wiemy jedynie to, że jest unikalna, niepowtarzalna. Aby wygenerować taką wartość, posłużymy się funkcją Symbol():


const sym = Symbol();
console.log(typeof sym); //symbol

Podczas generowania symbolu możemy podać dodatkową opcjonalną wartość, która będzie opisem danego symbolu. Opis ten nie zmienia wartości, a najczęściej używany jest do celów debugowania.


const a = Symbol("foo");
const b = Symbol("foo");
console.log(a === b); //false

Zastosowanie

Symbole przydają się w sytuacjach gdzie chcemy dodawać do obiektów dodatkowe funkcjonalności, a równocześnie nie chcemy się martwić o to, że przypadkowo w takim obiekcie coś nadpiszemy.


const a = "pet";
const sym1 = Symbol();

const ob = {
    [a] : "pies",
    [sym1] : "nowa ważna wartość",
};

console.log( ob[a], ob.pet, ob["pet"],  ); //"pies", "pies", "pies"

//W przypadku symbolu nie znamy wartości, jedyną możliwością jest użycie danego symbolu
console.log( ob[sym1] ) //"nowa ważna wartość"

Właściwości kryjące się za symbolem nie są dodatkowo iterowalne, więc idealnie nadają się do tworzenia "ukrytych" właściwości:


const sym = Symbol();

const ob = {
    name : "Karol",
    [sym] : "Super tajne hasło"
}

for (const key in ob) {
    console.log(key); //name
}

for (const key of Object.keys(ob)) {
    console.log(key); //name
}

Może się zdarzyć sytuacja, że nie będziemy mieli zmiennej z danym symbolem.


let sym = Symbol("a");
const ob = {
    [sym] : "tajna wiadomość"
}
console.log(ob); //{Symbol("a") : "tajna wiadomość"}

sym = Symbol("b"); //pod tą samą zmienną podstawiam nową wartość - straciliśmy referencję do powyższego symbolu!
ob[sym] = "inna wiadomość";
console.log(ob); //{Symbol("a") : "tajna wiadomość", Symbol("b") : "inna wiadomość"}

Podstawiając pod tą samą zmienną drugi symbol straciliśmy odniesienie do pierwszego.

W takiej sytuacji możemy skorzystać z metody Object.getOwnPropertySymbols(ob), która zwraca symbole użyte w danym obiekcie:


const symbols = Object.getOwnPropertySymbols(ob);

console.log(symbols); //[Symbol("a"), Symbol("b")]
console.log(symbols.length); //2

console.log(symbols[0]); //Symbol("a")
console.log(ob[symbols[0]]) //"tajna wiadomość"

Globalny rejestr symboli

Globalny rejestr symboli to takie miejsce, w którym możemy przechowywać globalne symbole, które są dostępne dla wszystkich skryptów (ale też oddzielnych środowisk, gdzie każde ma swój główny obiekt).

Może to przyrównać do listy, w której zapisujemy symbole, a dzięki temu możemy się do nich odwoływać z każdego miejsca.

Do manipulacji symbolami w tym rejestrze mamy 2 funkcje:

Symbol.for(key) Przeszukuje globalny rejestr i zwraca symbol zapisany pod danym kluczem. Jeżeli takiego symbolu nie było, tworzy pod nowy symbol
Symbol.keyFor(variable) Jeżeli dana zmienna wskazuje na symbol, możemy pobrać jego klucz z globalnego rejestru (lub undefined, jeżeli go tam nie ma)

Symbol.for("sample text"); //tworzę nowy symbol
Symbol.for("other text"); //tworzę nowy symbol

console.log(Symbol.for("sample text")); //pobieram wcześniej utworzony symbol

const a = Symbol.for("super hidden message");
const b = Symbol("test");

console.log(Symbol.keyFor(a)); //"super hidden message"
console.log(Symbol.keyFor(b)); //undefined

Dobrze znane symbole

Javascript zawiera kilka wbudowanych symboli, które zwą się "well know symbols.

Wprowadzają one dodatkowe funkcjonalności do istniejących od lat obiektów, ale też pozwalają modyfikować zachowanie niektórych działań jakie wykonujemy na co dzień.

Kryją się one jako właściwości funkcji Symbol(). Ich pełną listę znajdziesz na stronie MDN. W przeglądarce Firefox możesz też w konsoli wpisać nazwę funkcji Symbol, po czym zbadać jej wygląd: funkcja Symbol

Te najczęściej używane to:

  • Symbol.hasInstance
  • Symbol.toPrimitive
  • Symbol.isConcatSpreadable
  • Symbol.iterator

Dla uproszczenia symbole takie mają też skrótowe nazwy poprzedzone @@nazwa. I tak Symbol.hasInstance w skrócie nazywa się @@hasInstance, Symbol.iterator to @@iterator itp.

Do czego one służą? A właśnie sobie je omówimy.

Symbol.hasInstance

W rozdziale o dziedziczeniu w Javascript sprawdzaliśmy czy dana instancja jest egzemplarzem danej klasy za pomocą konstrukcji instanceof. Za pomocą tego symbolu możemy zmienić działanie operatora instanceof dla danej klasy:


class Car {
}

console.log([] instanceof Car); //false

Tablica oczywiście nie jest instancją klasy Car. Dzięki powyższemu symbolowi możemy dodać własny warunek testujący:


class Car {
    static [Symbol.hasInstance](obj) {
        return Array.isArray(obj); //dowolny test
    }
}

console.log([] instanceof Car); // true

Symbol.toPrimitive

Innymi dość ciekawymi Symbolem jest Symbol.toPrimitive. Opisuje go dobrze artykuł pod adresem https://blog.comandeer.pl/kwacze-jak-kaczka.html.

Używając obiektów dość często zachodzi konieczność konwersji ich na typy prymitywne. W przypadku boolean (np. obiekt stosowany jako wyrażenie wewnątrz funkcji if) obiekty zawsze zwracają true. Podczas innych operacji może zajść potrzeba konwersji na typ string lub number.


if ({}) { ... } //true

console.log({} + "!!!") //"[object Object]!!!""

Robiąc taką konwersję, Javascript stara się wykryć na jaki typ ma przeprowadzić konwersję. Preferowany wariant konwersji zwany "hint" może przyjąć trzy wartości: "string", "number" lub "default".

Domyślnie do takiej konwersji wykorzystywane są 2 funkcje: toString() oraz valueOf(), które każdy obiekt dziedziczy z prototypu wszystkich obiektów czyli Object.prototype. Funkcja toString() zwraca obiekt w postaci tekstu [object Object]. Funkcja valueOf() w większości przypadków zwraca obiekt sam w sobie:


const ob = {};

alert(ob); //"[object Object]"
ob.toString(); //"[object Object]"
ob.valueOf() === ob; //true

Podczas konwersji funkcje te odpalane są w kolejności toString(), valueOf() jeżeli "hint" wynosi "string", oraz w przeciwnej jeżeli "hint" wynosi inną wartość. Oznacza to, że w większości przypadków wynikiem konwersji będzie string, natomiast w nielicznych sytuacjach dostaniemy NaN gdy obiekt ma być skonwertowany na liczbę (ale nie zawsze, bo np. gdy odejmujemy od siebie datę, wynik będzie liczbą).


const a = {};
const b = {};

console.log(a + b); //"[object Object][object Object]"
console.log(Number(a) + Number(b)); //NaN

const date1 = new Date(2020, 10, 10);
const date2 = new Date(2019, 10, 10);
console.log(date1 - date2); //31622400000

Dzięki omawianemu symbolowi możemy dodać własną metodę, która będzie wykorzystywana przy konwersji.


const price = {
    value: 500
};

console.log( price + "PLN" ); //"[object Object]PLN"
console.log( Number(price) ); //NaN

const price = {
    value: 500,

    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case "string":
            case "default":
                return this.value.toString();
            break;
            case "number":
                return this.value;
            break;
        }
    }
}

console.log( price + "PLN" ); //"500PLN"
console.log( Number(price) ); //500

Symbol.isConcatSpreadable

Podczas łączenia dwóch tablic za pomocą concat() ich elementy są rozbijane na poszczególne kawałki.


const tab1 = ["a", "b", "c"];
const tab2 = ["d", "e", "f"];

console.log(tab1.concat(tab2)); //["a", "b", "c", "d", "e", "f"]

Dzięki temu symbolowi możemy włączyć lub wyłączyć rozbijanie danego obiektu na poszczególne części:


const tab1 = ["a", "b", "c"];
const tab2 = ["d", "e", "f"];

tab2[Symbol.isConcatSpreadable] = false;

console.log( tab1.concat(tab2) ); //["a", "b", "c", ["d", "e", "f"]]

Dla obiektów tablico podobnych (np. kolekcje elementów pobranych ze strony) zastosowanie tego symbolu umożliwi nam sprawienie, że podczas concatowania dana kolekcja będzie rozbijana na poszczególne elementy:


const collectionA = {
    0: "element1",
    1: "element2",
    length: 2
};

const collectionB = {
    0: "element1",
    1: "element2",
    length: 2,
    [Symbol.isConcatSpreadable]: true
};

console.log( ["a", "b"].concat(collectionA) ); //["a", "b", {…}]
console.log( ["a", "b"].concat(collectionB) ); //["a", "b", "element1", "element2"]

Symbol.iterator

Jak wiemy, bez problemu możemy robić klasyczne pętle for, a dzięki nim wyciągać poszczególne elementy z tablic ale i tablico podobnych struktur (w tym stringów):


const tab = ["a", "b", "c"];
for (let i=0; i<tab.length; i++) {
    console.log(tab[i]);
}

const txt = "abc";
for (let i=0; i<txt.length; i++) {
    console.log(txt[i]);
}

const buttons = document.querySelectorAll("button");
for (let i=0; i<buttons.length; i++) {
    console.log(buttons[i]);
}

Wraz z wprowadzeniem symboli dostaliśmy też możliwość stosowania bardzo wygodnej pętli iteracyjnej for of:


const tab = ["a", "b", "c"];
for (const el of tab) {
    console.log(el);
}

const txt = "abc";
for (const letter of txt) {
    console.log(letter);
}

const buttons = document.querySelectorAll("button");
for (const btn of buttons) {
    console.log(btn);
}

Pętla ta do wykonywania iteracji wykorzystuje omawiany symbol. Tablico podobne struktury (String, Array, Map, Set) mają pod tym symbolem wstawioną odpowiednią funkcję iterującą.


const tab = ["a", "b", "c"];
console.log( tab.prototype[Symbol(Symbol.iterator)] ); //function

Po bardziej rozbudowanych strukturach (obiektach) takiej pętli zrobić nie możemy, chyba, że podpowiemy Javascriptowi jak taką pętlę ma robić ustawiając dla nich ich własną metodę dla tego symbolu:


const ob = {
    pets: ["pies", "kot", "świnka"]
}

for (const el of ob) { //error: ob is not iterable
    console.log(el);
}

const ob = {
    pets: ["pies", "kot", "świnka"],

    *[Symbol.iterator]() {
        for (const el of this.pets) {
            yield el;
        }
    }
}

for (const el of ob) {
    console.log(el); //"pies", "kot"...
}

Temat tak naprawdę jest bardziej rozbudowany, dlatego poświęcimy mu kolejny rozdział.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę z tego działu, to zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania-es6

W repozytorium jest branch "solutions". Tam znajdziesz przykładowe rozwiązania.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem. Aha - i ta strona korzysta z ciasteczek.

Menu