
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).

#1 Dziedziczenie w JavaScript - wersja klasyczna
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.