SymbolNr.4Pogadajmy o symbolach, które są od ES6. Nie było ich wcześniej w tym języku programowania. Co pierwszy raz o nich słyszysz.  Nie ma problemu. Po to jest w końcu ten szybki trening JavaScript. Symbol spełnia parę zadań w JavaScript.

Jednym z nich jest wygenerowanie unikatowego identyfikatora. 

Sztuczna polega na tym, że TY nigdy nie wiemy, jak dokładnie wygląda ten identyfikator i nie masz do niego dostępu. 

Nie masz żadnej możliwości inspekcji, aby zobaczyć, czym ten identyfikator jest. Wiem, że będzie on unikatowy. Utwórzmy, więc ten symbol korzystając z funkcji symbol.

Jak widzisz, aby go utworzyć, muszę podać jakiś napis. Ten napis jest użyty tylko do celów debugowania.

Co się teraz stanie? Utworzy się nowy symbol z unikatowym ID w silniku JavaScript i przypiszemy ten identyfikator do naszej zmiennej.

Sprawdźmy jakiego typu jest nasz symbol.

let uniquEventSymbol = Symbol('click on a button');
console.log(typeof uniquEventSymbol);

@_01.png

Z konsoli dowiesz się, że typem jest 'symbol'. Jest to nowy typ w ES6.

Teraz chcemy sprawdzić, co zwróci metoda toString() dla tego symbolu.

let uniquEventSymbol = Symbol('click on a button');
console.log(uniquEventSymbol.toString());

@_02.png

W konsoli otrzymasz "Symbol(click on a button)"

Czyli możesz sie dowiedzieć, jak oryginalnie ten symbol powstał, jednakże  nie dowiesz się nic na temat tego, co on dokładnie przechowuje w sobie.

Logiczne jest używanie symboli ze stałymi. Tym razem nie korzystam z funkcji ToString(). Jak myślisz, co pojawi się w konsoli.

const CLICK_ON_BUTTON_ID = Symbol('click on a button');
console.log(CLICK_ON_BUTTON_ID)

W konsoli zobaczysz to samo co wcześniej, czyli : Symbol(click on a button). 

Ustalmy, jak definiowana jest taka unikatowość symbolu.  Co się stanie jak utworze dwa symbole z tym samym napisem? Będą one różne czy takie same.

let s1 = Symbol('event TouchSreen');
let s2 = Symbol('event TouchSreen');

console.log(s1 === s2);

@_03'.png

Z konsoli dowiesz się, że oba symbole są różne i unikatowe, czyli dostaliśmy : false.

Jak się jednak odwołać do symboli, które już utworzyłeś. Użyć zmiennych i stałych? Istnieje jeszcze wbudowany rejestrator symboli. 

Jest to funkcja statyczna Symbol.for. W niej przekazujesz klucz do zarejestrowanego wcześniej symbolu. Jeśli takiego symbolu nie to zostanie on utworzony.

let s1 = Symbol.for('event TouchSreen');
let s2 = Symbol.for('event Click');

console.log(s1);
console.log(s2);

@_04.png

Taki kod jest prawidłowy. Sprawdźmy teraz, czy identyfikator są sobie równe, gdy wyciągamy je z rejestratora.

let s = Symbol('event Click');

let s1 = Symbol.for('event Click');
let s2 = Symbol.for('event Click');

console.log(s1 === s2);

console.log(s === s1);
console.log(s === s2);

@_05.png

Ten kod pokaże Ci, że o ile tworzenie/wyciąganie symboli działa tak jakbyś się spodziewał. To tworzenie domyślnie symbolu tak jak robiliśmy to wcześniej. Automatycznie nie dodaje go do rejestru. Co sprawia, że dwa ostatnie logiczne warunki zwrócą fałsz.

Czy używając rejestru, możesz utworzyć do niego klucz?

let key = Symbol.for('event closingWindow');
let description = Symbol.keyFor(key);
console.log(description);

@_06.png

Tak tylko oczywiście nie dostaniesz, tak żadnej informacji co ten symbol trzyma w sobie.

Teraz możesz zadać sobie pytanie: "po co w ogóle ten symbol istnieje". Możemy porównywać jeden symbol z drugim i sprawdzać, czy są sobie równe. Kiedy ma sens ich użycie?

Możesz utworzyć obiekt z właściwością, którego nazywa jest "symbolem". Alternatywnie obiekty w JavaScript można traktować jak słowniki i kluczem do wartości pewnego obiektu będzie właśnie symbol.

Symbole w obiektach w pewnym sensie symulują prywatne pola, do których masz dostęp, tylko jeśli znasz odpowiedni symbol.

Mogą Cię też zabezpieczyć Cię przed kolizją nazw.

Spójrz na ten kod. Jak pamiętasz. Przy tworzeniu obiektu możemy używać "wyrażeń", więc taki kod jest całkowicie poprawny. Musimy tylko dodać to wyrażenie w nawiasach kwadratowych.

let blog = {
    title : 'Blog Name',
    [Symbol.for('Szybki trening JS')] : 'content 1',
    [Symbol.for('TDD z C#')] : 'content 2',
};

let value = blog[Symbol.for('TDD z C#')];
console.log(value);

@_07.png

W konsoli zobaczysz wartość : content 2

Teraz sobie myślisz zapewne zaraz zaraz, a przypadkiem teraz nie powinien wiedzieć, jaki ma identyfikator ten symbol. Przecież mogę zobaczyć wszystkie właściwości obiektu co nie.

let blog = {
    title : 'Blog Name',
    [Symbol.for('Szybki trening JS')] : 'content 1',
    [Symbol.for('TDD z C#')] : 'content 2',
};

console.log(Object.getOwnPropertyNames(blog));

@_08.png

Tutaj konsole Ciebie zaskoczy, bo dostaniesz

['title']

Czyli wszystkie nasze symbole zostały zignorowane. Nie możesz więc uzyskać dostępu do symbolu w taki sposób.

Istnieje jednak inna nowa metoda statyczna, która może to zrobić.

let blog = {
    title : 'Blog Name',
    [Symbol.for('Szybki trening JS')] : 'content 1',
    [Symbol.for('TDD z C#')] : 'content 2',
};

console.log(Object.getOwnPropertySymbols(blog));

@_09.png

Dostaniesz tablice:

[Symbol(Szybki trening JS), Symbol(TDD z C#)]

Oczywiście znowu nie dostanie identyfikatora symbolu tylko napis, który go utworzył.

Znane symbole

Zastosowania symboli tutaj się jednak nie kończą. Istnieje szereg gotowych i znanych symboli, które mogą być wykorzystane przez Ciebie do meta programowania.

Meta programowanie polega na głęboki spojrzeniu w tym, jak obiekty, funkcje oraz sam silnik JavaScript działa. Istnieje szereg symboli, które możemy użyć, aby uzyskać dostęp do unikatowych działań silnika JavaScript, które istnieją dopiero od wersji ES6.

Jednym z takich symboli można zobaczyć, gdy użyjesz funkcji "ToString()" na funkcji. Jak myślisz, co taki kod pokaże:

let Game = function() {

};

let game = new Game();
console.log(game.toString());

@_10.png

W konsoli zobaczysz : [object Object]

Teraz co byś powiedział na zmianę funkcji ToString() dla tego obiektu, by pokazać coś lepszego. Normalnie nie byłoby to możliwe, ale hej, od czego masz symbole.

Mamy naszą funkcję Game. Używając "prototype" uzyskam dostęp do pola, które definiuje, to co ma się wydarzyć w metodzie toString().

Teraz gdy nadpisaliśmy zachowanie, to co się wydarzy w takim kodzie?

let Game = function() {

};
Game.prototype[Symbol.toStringTag] = 'Game Class Pacman';

let game = new Game();
console.log(game.toString());

@_11.png

Dostaniemy teraz [object Game Class Pacman]. Taki napis może być bardziej pomocny.

Oto przykład prostego meta programowanie w JavaScript. Zmieniliśmy rezultat wywołania procedury toString() w siniku JavaScript dla tego obiektu.

Istnieje kolejny bardzo dobrze znany symbol i ma on związek  z łączeniem tablic i napisów (są tablicą znaków). Spójrz na ten kod. 

Oto przykład działania funkcji concat.

let values1 = [1,2,3];
let values2 = [4,5,6]
console.log([].concat(values1).concat(values2));

@_12.png

Do pustej tablicy dodajesz wartości, więc w konsoli pojawi się to : [1,2,3,4,5,6]

Teraz korzystając z symbolu "isConcatSpreadable" możemy ustalić, jak dokładnie mają wartości do tablic się dodawać.

Ustawiając, tę wartość na false sprawisz, że każda tablica dodana przez tę funkcję nie będzie ulegać rozproszeniu na mniejsze elementy.

Co teraz pojawi się w konsoli.

let values1 = [1,2,3]
let values2 = [4,5,6];

values1[Symbol.isConcatSpreadable] = false;
values2[Symbol.isConcatSpreadable] = false;
console.log([].concat(values1).concat(values2));

@_13.png

Dostaniesz tablice tablic [[1,2,3],[4,5,6]]. Jak widzisz, dodaliśmy dwa elementy do pustej tablicy, które są także swoją tablicą.

Co powiesz na kolejny fajny symbol. Spójrz na ten przykład.

Mamy tutaj tablice i próbujemy do niej dodać wartość 100. Co jednak tutaj się stanie?

let values = [9,9,9];

let sum = values + 100;
console.log(sum);

@_14.png

W konsoli dostaniesz dziwny napis : 9,9,9100

Najpierw mamy wartości, które znamy, a później nasze 100 zostało dodane do ostatniej wartości tablicy jako napis. Dlatego mamy wartość 9100.

Jak widać, coś tu nie działa. Czy możemy uzyskać nad tym kontrole?

Skorzystajmy z symbolu "toPrimitive" jak widzisz, może ona przyjąć funkcję z parametrem pomocniczym. Zobaczmy, co ten parametr pomocniczy w sobie zawiera. 

Zwrócimy w tej funkcji liczbę 27. Jest to suma trzech dziewiątek w naszej tablicy.

Później tak jak wcześniej do tej tablicy dodamy wartość 100. Co pojawi się w konsoli?

let values = [9,9,9];

values[Symbol.toPrimitive] = function (hint) {
    console.log(hint);
    return 27;
};

let sum = values + 100;
console.log(sum);

@_15.png

Nasza wskazówka zwróciła wartość "default". Natomiast suma wynosi 127. Stało się tak, ponieważ gdy zaszła operacja dodawania nasza funkcja została wywołana w przeciwieństwie do domyślnego zachowania.

Takich gotowych symboli jest dużo więcej.

Oto symbole dla iteracji:

  • Symbol.asyncIterator
  • Symbol.Iterator

Oto symbole dla wyrażeń regularnych:

  • Symbol.match
  • Symbol.matchAll
  • Symbol.replace
  • Symbol.search
  • Symbol.split

Inne symbole to

  • Symbol.hasInstance
  • Symbol.species
  • Symbol.unscopables
  • Symbol.toStringTag
  • Symbol.isConcatSpreadable
  • Symbol.toPrimitive

To tylko były proste przykłady. Teraz omówmy gotowe funkcje dla wielu klas w JavaScript, które warto znać.