Ostatnio w projekcie natrafiłem na problem, który najlepiej było rozwiązać stosując dziedziczenie. Niby prosta sprawa, “podstawowe podstawy” każdego obiektowego języka programowania. Jak jednak się pewnie domyślacie, dziedziczenie w JavaScript “nieco” różni się od tego klasycznego podejścia, znanego z innych języków programowania, a ja zdałem sobie sprawę, że choć na tym blogu poruszam różnorakie (i na różnym poziome skomplikowania) zagadnienia związane z JavaScriptem to tematyka dziedziczenia w tym języku nigdy się jakoś nie pojawiła… Śpieszę więc naprawić to niedopatrzenie i dziś postanowiłem odsłonić przed Wami tajniki tej podstawowej techniki programistycznej w najważniejszym języku programowania webowego! Temat jest na tyle szeroki, że postanowiłem zrobić z tego krótką serię wpisów, a dziś jej pierwsza część (na tę chwilę nie jestem pewien ile będzie tych części - co najmniej dwie lub trzy).

Dziedziczenie w JavaScript - prototypy

Język JavaScript, w odróżnieniu od innych języków nie posiada pojęcia klasy. Tutaj wszystko jest obiektem, nawet (a raczej przede wszystkim) funkcje, a zamiast dziedziczenia opartego na hierarchii klas mamy dziedziczenie prototypowe. Dlatego zanim zacznę omawiać główny temat tego wpisu, najpierw kilka słów o prototypach. Ogólnie rzecz biorąc, “każden jeden” obiekt w JavaScripcie powiązany jest z obiektem prototypu - dla przykładu każda funkcja, którą utworzymy od razu posiada właściwość prototype, która zawiera nowy pusty obiekt:

function test(name){
  return name;
}

alert(typeof test.prototype); // zwraca 'object'
alert(typeof test.prototype.constructor); // zwraca 'function'
alert(test.prototype.constructor('dupa')); // zwraca 'dupa'

Jak widać powyżej, właściwość prototype zawiera w sobie właściwość constructor, która to z kolei wskazuje na utworzoną funkcję - czyli w przykładzie funkcję test (dlatego też powyższy kod w ostatniej linii zwraca wartość przekazaną do konstruktora). Tak utworzony obiekt prototypu możemy teraz wykorzystać do przypisywania do niego właściwości i funkcji - będą one współdzielone przez obiekty dziedziczące. Zobaczmy więc jak to się robi.

Dziedziczenie - wersja klasyczna

W razie jakby ktoś się nie domyślił, powyższy podtytuł ma sugerować, że prezentowane dalej podejście ma imitować podejście znane z innych języków programowania… :P Tak jak wspomniałem w punkcie dotyczącym prototypów, po utworzeniu funkcji tworzona jest właściwość constructor wskazująca na tę funkcję. Wspomniałem też, że właściwość prototype to miejsce, gdzie definiujemy wszystkie właściwości i funkcje, które mają zostać odziedziczone (współdzielone). Z tego wniosek, że każda funkcja może być odziedziczona! W sumie słusznie - nie ma przecież żadnego mechanizmu mówiącego co ma być dziedziczone, a co nie.

Mając w głowie powyższe informacje, możemy przejść dalej. Generalnie, to co upodabnia dziedziczenie w JavaScript do języków programowania posiadających klasy jest słowo kluczowe new, które za chwilę użyjemy. To dlatego mówimy, że opisywane teraz podejście do dziedziczenia jest “classic”. Oględnie mówiąc - użycie new do wywołania funkcji zmienia jej zachowanie. Zamiast wywołać ją bezpośrednio, tworzony jest nowy obiekt, którego właściwość prototype jest wiązana z właściwością prototype tejże funkcji. Ponadto this jest wiązane z nowo powstałym obiektem więc wszystkie odwołania do this w tej funkcji odbywają się tak na prawdę na rzecz nowo utworzonego obiektu (bo po tych wszystkich operacjach wszystkie instrukcje w funkcji również są wywoływane).

Przykład

Wiem, wiem… Trochę to zagmatwanie brzmi. Może lepiej przykład?

See the Pen ByYErz by burczu (@burczu) on CodePen.

Skupmy się na zakładce JS powyższego codepena. Najpierw definiujemy funkcję, która posłuży nam jako konstruktor - zwróćcie uwagę, że nazwę funkcji piszemy z wielkiej litery. To taka konwencja mająca na celu odróżnić zwykłe funkcje od tych, które są zaprojektowane do konstruowania obiektów.

Kolejna rzecz to definicja funkcji, którą przypisujemy do właściwości prototype. Jak już wcześniej wspominałem, będzie ona dostępna dla obiektu, który odziedziczy funkcję Base, dlatego warto zwrócić uwagę, że zwraca ona wartość this.name - tylko na co będzie wskazywać this? Na koniec wywołujemy funkcję Base z użyciem słowa kluczowego new. Tak jak już pisałem - tutaj wykonywane jest tworzenie nowego obiektu, wiązanie jego prototypu z prototypem funkcji Base, a na koniec wywoływanie instrukcji funkcji Base tyle, że this wskazuje już na nowy obiekt.

W rezultacie obiekt ten przypisywany jest do zmiennej test. Dalej mamy już tylko kod wyświetlający wyniki w zakładce Result codepena - jak widać obiekt test posiada dostęp do funkcji getName, która przecież pochodzi z prototypu funkcji Base. Jako, że wywołujemy ją w kontekście obiektu test, to this w jej ciele wskazuje właśnie na ten obiekt. Mam nadzieję, że nie jest to wszystko dla Was zbyt zawiłe :D Warto spojrzeć też na zakomentowany kawałek kodu - pokazuje on, że obiekt test posiada właściwość name. To dowód na to, że rzeczywiście kod funkcji Base został wywołany na rzecz obiektu przypisanego później do zmiennej test. To podstawa zrozumienia dziedziczenia w JavaScripcie - będę ten temat jeszcze rozwijać w kolejnych częściach tego cyklu wpisów.

Podsumowanie

Moim zdaniem warto znać podejście “classic” do dziedziczenia w JavaScript jednak nie jest to najszczęśliwszy element tego języka. Zastosowanie słowa kluczowego new dość mocno zaciemnia obraz, szczególnie osobom, które mają więcej do czynienia z klasycznymi językami programowania - niesłusznie sugeruje ono zachowanie oparte na hierarchii klas i trzymaniu typów. Tymczasem w JavaScript jest to bardziej zmienianie kontekstu wywołania funkcji, natomiast dzięki prototypom uzyskujemy coś w rodzaju polimorfizmu. Dlatego też nie zaleca się stosowania opisanego dziś podejścia. W kolejnych wpisach poznamy lepsze sposoby na dziedziczenie w JavaScript.