Rozszerzanie klas bazowych #64
Wprowadzenie klas w ES6 i słowa kluczowego extends
dało łatwiejsze możliwości rozszerzani klas bazowych, czego do wersji ES5 nie dało się tak łatwo i przyjemnie osiągać. Od teraz możemy rozszerzać takie klasy wbudowane jak Array
, Error
, Date
a nawet Object
i zachować pełną funkcjonalność obiektów bazowych, tego właśnie brakowało w ES5.
Dziedziczenie po Array
Może się zdarzyć, że będziemy w naszej aplikacji potrzebować tablice o specjalnym działaniu. Najlepszym sposobem do osiągnięcia tego w JavaScript jest po prostu rozszerzenie klasy Array
:
class MyArray extends Array {
toString() {
return this.join('-');
}
mapDouble() {
return this.map((x) => x * 2);
}
}
Stworzyłem klasę i za pomocą słowa kluczowego extends
rozszerzam wbudowaną w JavaScript klasę Array
. W mojej nowej klasie dodatkowo nadpisuję metodę toString()
, która pochodzi z klasy Array
. Dodaje także zupełnie nową metodę, która będzie zwracała tablice z podwojonymi wartościami.
Zobaczmy jak pracować z taką własną tablicą:
const arr = new MyArray(1, 2, 3, 4);
console.log(arr.length); // 4
console.log(arr.filter(x => x % 2 === 0)); // [2, 4]
console.log(arr.toString()); // 1-2-3-4
console.log(arr.mapDouble()); // [2, 4, 6, 8]
Jak przy każdej klasie, powołuję obiekt przez wywołanie new MyArray()
i do konstruktora podaję wartości oddzielone przecinkiem. Oczywiście nie robimy inicjalizacji w formie literalnej, wywołanie konstruktora jest tutaj potrzebne.
Na obiekcie pochodzącym z mojej klasy mogę wywołać nie tylko moje metody, ale wszystkie, które pochodzą z Array.prototype
. Mam tutaj dostęp do właściwości length
, mogę wywołać metodę filter
. Mam nadpisaną metodę toString()
oraz własną nową metodę mapDouble
().
W ten sposób stworzyłem własną wersję tablic w JavaScript. To właśnie w takich sytuacjach rozszerzanie klas i dziedziczenie właściwości pokazuje swoje ogromne możliwości.
W mojej klasie nie stworzyłem konstruktora, korzystam z domyślnego wywołania, jeżeli chciałbym umieścić konstruktor wyglądałby on tak:
constructor(...items)
{
super(...items);
}
Jeżeli zatem chcemy stworzyć własną funkcjonalność dla tablic, możemy stworzyć swój typ i posługiwać się nim w całej aplikacji. Jest to o wiele lepsze rozwiązanie niż modyfikowanie Array.prototype
. Od wersji ES6 w JavaScript proces rozszerzania wbudowanych obiektów JavaScript jest o wiele
łatwiejszy.
Tworzenie własnych błędów
Rozszerzenie wbudowanej klasy Error
może być najczęściej spotykanym przypadkiem wykorzystania dziedziczenia w JavaScript. Co prawda jest wbudowanych kilka klas do obsługi pojawiających się błędów, ale czasem chcemy mieć bardziej szczegółowe informacje, co się stało:
class EmptyArrayError extends Error {
constructor(message) {
super(message);
this.name = 'EmptyArrayError'
}
}
Dlatego tworzę własną klasę o nazwie EmptyArrayError
, która rozszerza bazową klasę Error
. Klasa Error
jest bazową klasą dla wszystkich błędów w JavaScript. Dodatkowo tworzę konstruktor, ale nie muszę tego robić. Chcę jednak bardziej dopasować klasę do moich potrzeb.
Tworząc konstruktor muszę wywołać super()
i przekazać tam message
, jest to wartość typu string informująca nas jaki błąd powstał. Tworzę także pole name
, a tak naprawdę nadpisuję pole name
bo istnieje ono także w klasie Error
, chce jednak aby to pole reprezentowała nazwę mojego błędu.
Standardowo w klasie Error
, pole name
ustawione jest na nazwę 'Error'
jako string. Tutaj przypisuję po prostu nazwę mojego błędu.
Tak przygotowaną klasę mogę teraz wykorzystywać w swoim kodzie
const array = [];
if (array.length === 0) {
throw new EmptyArrayError('Array should not be empty');
}
Hipotetyczny przypadek. W jakiejś części kodu tablica nie może być pusta. Instrukcja if
sprawdza, czy tablica jest pusta, jeżeli tak to wyrzucam błąd przez użycie throw new EmptyArrayError()
i do konstruktora podaję komunikat. Dzięki temu w konsoli pojawia się błąd:
Uncaught EmptyArrayError: Array should not be empty
Widzimy, że jest to błąd pochodzący z naszej klasy i z naszym komunikatem. Takie klasy z błędami mogą się przydać w wielu miejscach aplikacji, gdy chcemy zareagować na niestandardową sytuację. Każda aplikacja wymaga obsługi błędów, możemy więc tworzyć kolejne klasy błędów jak AccesError
, ValidationError
, ReadOnlyError
i tym podobne.
Rozszerzane innych klas, szczególnie wielopoziomowe, to kolejne stopnie skomplikowania kodu. Musimy rozważnie używać tych możliwości. W wielu przypadkach jednak może nam to usprawnić działanie aplikacji i poszerzyć możliwości jak w przypadku obsługi błędów. Przy językach obiektowych, jest to jednak
coś, co musimy opanować.
Co warto zapamiętać:
- w ES6 dziedziczenie po wbudowanych klasach w JavaScript jest łatwiejsze niż wcześniej
- można na przykład rozszerzyć klasę
Array
i stworzyć własną tablicę - rozszerzenie klas bazowych daje ogromne możliwości na przykład tworzenia własnych błędów aplikacji
- dziedziczenie zawsze wprowadza kolejny stopień skomplikowania, używajmy tam, gdzie musimy, a nie dlatego, że jest fajne
Operator typeof vs instanceof w JavaScript
Na początku tego kursu pracowaliśmy głównie z podstawowymi typami w JavaScript. Do ich sprawdzania używałem operatora typeof
. W JavaScript istnieje jeszcze jeden operator instanceof
, który użyłem kilka razy przy omawianiu prototypów. Zobaczmy, jaka jest różnica i który operator warto używać.
Operator typeof
Za pomocą operatora typeof
możemy wyświetlić typ sprawdzanej wartości lub też wykorzystać go do porównania czy dana wartość jest określonego typu:
console.log(typeof 'boo'); // 'string'
Gdy użyjemy typeof
do sprawdzenia jakiego typu jest wartość, zostanie zwrócony typ w postaci stringa.
Dlatego, gdy wykorzystujemy ten operator do porównania, musimy typy zapisywać jako string:
console.log(typeof 'foo' === 'string'); // true
console.log(typeof 42 === 'number'); // true
console.log(typeof 42n === 'bigint'); // true
console.log(typeof true === 'boolean'); // true
console.log(typeof null === 'object'); // true
console.log(typeof undefined === 'undefined'); // true
console.log(typeof function() {
} === 'function'); // true
console.log(typeof Symbol('Sym') === 'symbol'); // true
console.log(typeof [] === 'object'); // true
console.log(typeof {} === 'object'); // true
W ten sposób za pomocą typeof
i zwykłego operatora porównania możemy sprawdzić, czy dana wartość ma konkretny typ. Nazwa każdego typu zapisana jest w formie tekstu.
Operator typeof
potrafi zwrócić tylko te typy, które są w budowane w JavaScript. Zwraca więc wszystkie typy prymitywne, dodatkowo potrafi rozróżnić typ object
i function
.
Nie potrafi jednak dokładnie określić typu na podstawie klasy:
console.log(typeof new Date()); // 'object'
console.log(typeof new Error('error')); // 'object'
console.log(typeof new Array()); // 'object'
console.log(typeof new String('boo')); // 'object'
console.log(typeof new Number(42)); // 'object'
class Foo {
}
console.log(typeof new Foo()); // 'object'
Mamy tutaj przykład klas wbudowanych jak Date
, Error
czy Array
. Dla tych typów zawsze będzie zwracał typ object
. Nawet jeżeli stworzymy własną klasę, typeof
nie będzie umiał rozpoznać typu Foo
, również będzie zwracał typ object
. Oczywiście jest to także prawda, ponieważ każdy obiekt w JavaScript również ma typ Object
co jest związane z dziedziczeniem prototypowym.
Małe podsumowanie tego operatora. Operator typeof
na podstawie wartości podstawionej z prawej strony, zwraca typ tej wartości w formie string. Jest to albo jeden z typów prostych, podtyp function
albo typ object
. Operuje tylko na typach zdefiniowanych w JavaScript. Nie potrafi różnić
konkretnych klas jak Date
, Error
, Array
czy nasze własne klasy. Dla niego wszystkie te konstrukcje będą typu object
.
Operatora instanceof
Operator instanceof
z technicznej strony sprawdza łańcuch prototypów, aby dokładnie rozpoznać, w jaki sposób powstał dany obiekt, a więc używamy go tylko do testowania obiektów:
console.log(new Date() instanceof Date); // true
console.log(new Array() instanceof Array); // true
class Boo {
}
console.log(new Boo() instanceof Boo); // true
Na tym przykładzie widzimy, że za jego pomocą możemy dokładnie określić, na jakiej bazie klasy powstał obiekt (a dokładniej z jakiego konstruktora funkcji). Stworzony obiekt Date
jest rozpoznawalny jako klasa Date
, tak samo jest z obiektem Array
oraz naszym obiektem zbudowanym z klasy Boo
.
Ten operator zwraca zawsze wartość boolean
, musimy więc podstawić obiekt do testowania, a także typ, z którym chcemy porównać dany obiekt. Operator nie wyświetli nam typu jak to robi operator typeof
.
Wspominałem, że operator potrafi przetestować cały łańcuch prototypów:
console.log(new Date() instanceof Object); // true
console.log(new Array() instanceof Object); // true
class Bar extends Boo {
}
console.log(new Bar() instanceof Boo); // true
console.log(new Bar() instanceof Bar); // true
console.log(new Bar() instanceof Object); // true
W tym przypadku widzimy, że obiekty powstałe z Date
oraz Array
to także typy Object
. Ponieważ na końcu ich łańcucha jest dołączony Object.prototype
.
Podobnie jest z naszą klasą Bar
, która rozszerza klasę Boo
. Obiekt klasy Bar
zbudowany jest więc z trzech prototypów, dlatego dla każdej z klas, które są przekazane do porównania zwróci true
.
Wspomnę jeszcze raz, że instanceof
nie działa na typach prymitywnych:
console.log('abc' instanceof String); // false
console.log(new String('abc') instanceof String); // true
Nie możemy prymitywnej wartości 'abc' porównać do typu prymitywnego 'string'
, który zwraca choćby operator typeof
. Po prawej stronie operatora instanceof
musi stać jakiś konstruktor funkcji czy nasza klasa.
Dlatego, jeżeli stworzymy obiekt za pomocą new String()
to możemy go porównać do konstruktora String
. Należy jednak pamiętać, że prymitywny string
, a obiekt stworzony za pomocą new String()
to dwa różne byty, z którymi nie pracuje się tak samo.
Kiedy jaki operator używać?
Nie ma jednoznacznej odpowiedzi, który operator najlepiej użyć. Jeżeli musimy przetestować typy prymitywne to używamy operatora typeof
.
Jeżeli pracujemy na własnych obiektach zdecydowanie przydatniejszy jest operator instanceof
, który dokładnie potrafi określić, jaki konstruktor został użyty do stworzenia obiektu.
Mówi się też, że typeof
jest szybszy niż instanceof
. Głównym powodem wolniejszego działania jest to, że instanceof
przeszukuje cały łańcuch prototypów. Dla bardzo skomplikowanych obiektów może to być dość pracochłonne zadanie, dla mniej skomplikowanych, nie powinniśmy się przejmować.
Co warto zapamiętać
-
operator
typeof
zwraca typ w postacistring
-
operator
instanceof
służy do porównania czy coś jest danego typu i zwraca wartośćtrue
lubfalse
-
operator
typeof
może zwrócić tylko jeden z kilku typów wbudowanych JavaScript -
operator
instanceof
pracuje tylko na typach obiektowych, nie sprawdzi typu dla wartości prymitywnej -
operatora
typeof
dla typów obiektowych zwraca tylkofunction
lubobject
, nie określi dokładnie typu stworzonego z klasyDate
, zawsze będzie toobject
-
operator
instanceof
może pomóc w dokładnym określeniu typu również dla obiektów tworzonych z naszych klas