Rozszerzanie klas bazowych

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);
  }
}
1
2
3
4
5
6
7
8
9

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]
1
2
3
4
5
6

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);
}
1
2
3
4

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'
  }
}
1
2
3
4
5
6

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');
}
1
2
3
4

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
1

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