Dziedziczenie w Javascript

Co to jest dziedziczenie

Do tej pory używaliśmy różnych typów danych. Poza null i undefined, każdy z typów pozwalał nam wykonywać na nich jakieś operacje. Przykładowo dla tekstów mogliśmy pobrać ich długość, pobrać ich wycinek czy zwrócić ich wersję pisaną dużymi literami. Dla tablic mogliśmy dodawać czy odejmować elementy za pomocą odpowiednich metod, a dla liczb przykładowo mogliśmy sformatować daną liczbę do odpowiedniej ilości miejsc po przecinku.

Skąd te wszystkie funkcjonalności się wzięły?

Programowanie obiektowe charakteryzuje się pewnymi mechanizmami. Jednym z podstawowych jest tak zwane dziedziczenie.

Gdybym zapytał ciebie czym jest dziedziczenie w realnym świecie co byś powiedział?

Dzieci dziedziczą po rodzicach pewne cechy. Może to kolor włosów, może kolor oczu, a może talent do rysowania.

Część takich właściwości sobie pobierają od rodziców, ale i też mają swoje własne.

Tata był przystojny. Syn też jest. Ale syn jest o wiele wyższy od ojca. Córka po mamie odziedziczyła spojrzenie, ale dodała swoje wyjątkowe różowe policzki.

Niektóre właściwości też nadpisują, bo na przykład córka ma włosy rude po rodzicu, ale jakieś takie nie za bardzo.

Co ważne w drugą stronę to nie działa. Rodzice nigdy nie dziedziczą po dzieciach (w realnym świecie czasami tak, ale to temat dla prawników).


To może inny przykład? Wyobraź sobie, że robimy grę - np. podobną do R-Type. My - jako statek lecimy w prawą stronę, z której nadlatuje na nas stado przeciwników.
Każdy taki przeciwnik powinien mieć właściwości takie jak pozycję, szybkość, swoją grafikę, siłę ataku plus wiele, wiele innych. Dodatkowo powinien mieć metody takie jak latanie, zniszczenie itp. Takich przeciwników chcielibyśmy mieć kilkanaście typów. Wszystkie te typy powinny mieć podobne funkcjonalności, ale równocześnie też każdy taki typ powinien dodawać coś od siebie. Jedni by strzelali, drudzy latali ale szybciej, inni byli by bardzo wytrzymali.
Oczywiście moglibyśmy tutaj zastosować słynną technikę każdego programisty czyli copy-pasta, ale było by to bardzo niewygodne dla nas. Wyobraź sobie, że napisałeś już funkcjonalność latania, skopiowałeś do 10 innych typów obiektów, po czym okazało się, że na samym początku popełniłeś jakąś literówkę. Cała zabawa od początku.
I tu właśnie przychodzi z pomocą dziedziczenie. Zamiast głupio kopiować, możemy sobie utworzyć typ Enemy, a następnie inne typy jak EnemyStrong, EnemyFast, EnemyShoot itp, które będą dziedziczyć po tamtym podstawowe funkcjonalności przeciwnika, a i będą mogły dodać lub zmienić dane funkcjonalności.

dziedziczenie obiektów

Dziedziczenie w Javascript

Podobnie jest także w Javascript. Gdy tworzysz jakiś typ danych, możesz dla niego odpalać różne funkcjonalności. Takie możliwości nie biorą się z nieba, a są właśnie dziedziczone.

W Javascript występuje tak zwane dziedziczenie prototypowe. Oznacza to, że każdy (są wyjątki od tej zasady) obiekt dziedziczy właściwości i metody z innego obiektu - tak zwanego prototypu.

Sprawdźmy to na przykładzie:


const cat = {
    name : "Kotik"
}

console.dir(cat);

Gdy zbadasz powyższy kod w konsoli, zobaczysz, że poza właściwością name, nasz obiekt ma specjalną właściwość [[Prototype]] (patrz poniższa ramka).
Jest to referencja dodawana przez JavaScript każdemu obiektowi (z małymi wyjątkami), a która wskazuje właśnie na prototyp danego obiektu (inny obiekt), z którego nasz obiekt dziedziczy jakieś funkcjonalności.

obiekt cat

Wskazanie na prototyp obiektu może być wyświetlane w debugerze pod różną postacią. I tak przez wiele lat w wielu przeglądarkach można je było zobaczyć pod nazwą __proto__. W przeglądarce Safari widnieje pod nazwą "Prototyp nazwa", w Firefoxie na Ubuntu to "prototype", natomiast w najnowszych wersjach przeglądarek pojawia się pod nazwą [[Prototype]].

Oryginalnie w dokumentacji EcmaScript używana jest nazwa [[Prototype]]. Większość przeglądarek chcąc ułatwić życie programistom poszło swoją drogą i zaimplementowało odwołanie się do takiego prototypu pod zmienną o nazwie __proto__.

Odwoływanie się do prototypu za pomocą właściwości __proto__ nigdy nie było zalecane, natomiast poprzez fakt, że tak wiele przeglądarek używało i dalej używa tej właściwości, w 2015 roku została ona wprowadzona do dokumentacji jako tak zwane "legacy" (jest, ale raczej nie używaj, bo może kiedyś zostanie usunięte).

W różnych przeglądarkach odwołanie do prototypu może przyjąć nieco inny wygląd. Dlatego też chcąc się do niego odwołać powinieneś używać przeznaczonych do tego metod czyli Object.getPrototypeOf() i ewentualnie Object.setPrototypeOf() gdy chcesz ustawić dla obiektu nowy prototyp.


Sprawdźmy to na przykładzie:


const cat = {
    name : "Kotik"
}

console.dir( cat );
console.log( cat.toString() ); //[object Object]

W powyższym kodzie dla naszego kota odpaliłem metodę, której sam w sobie nie ma. Jeżeli cokolwiek używamy dla danego obiektu, JavaScript początkowo szuka tej funkcjonalności bezpośrednio w danym obiekcie. Jeżeli ją znajdzie - użyje jej. Jeżeli nie - przejdzie do prototypu danego obiektu i tam spróbuje tej ją znaleźć i użyć.
Prototyp obiektu także jest obiektem, więc także dostał swój prototyp. W razie potrzeby Javascript może więc przejść do kolejnego obiektu i tam poszukać danej funkcjonalności. Sytuacja taka będzie się powtarzać, aż do momentu w którym Javascript odnajdzie daną metodę lub właściwość, lub dojdzie do ostatniego obiektu w hierarchii, który już swojego [[Prototype]] już nie ma, a w zasadzie ma ustawione na null.

Możesz to spokojnie sprawdzić w konsoli wrzucając do niej powyższy kod i rozwijając poszczególne prototypy.


Opisana powyżej zasada "wędrówki" po metodach tyczy się praktycznie każdego typu danych w Javascript (wyjątkiem jest undefined i null + nasze umyślne działania).

Większość typów danych możemy tworzyć za pomocą tak zwanych literałów (skrócony zapis), ale też za pomocą tak zwanych konstruktorów.


const obA = {}
const obB = new Object();

const boolA = true;
const boolB = new Boolean(true);

const tabA = ["Ala", "Bala"];
const tabB = new Array("Ala", "Bala");

const txtA = "Ala ma Kotika";
const txtB = new String("Ala ma Kotika");

Gdy utworzymy obiekty danego typu - np. Array, jego prototypem będzie obiekt, na który wskazuje też właściwość prototype znajdująca się w konstruktorze danego typu. Taki obiekt-prototyp zawiera zbiór właściwości i metod, z których obiekty danego typu mogą korzystać. Dzięki temu tablice mogą używać metod tablicowych (np. push(), pop()), teksty odpowiednich metody dla stringów (np. toUpperCase()) itp.


const tab = [1,2,3];
//właściwość __proto__ wskazuje na prototyp danego obiektu
//ale nie chcemy raczej jej używać na rzecz Object.getPrototypeOf()
Object.getPrototypeOf(tab) === tab.__proto__ //true
Object.getPrototypeOf(tab) === Array.prototype //true
tab.push === Array.prototype.push //true

const txt = "Ala ma kota";
Object.getPrototypeOf(txt) === String.prototype //true
txt.toUpperCase === String.prototype.toUpperCase //true

Powyższe prototypy danych typów danych same w sobie są obiektami, a więc są bytami typu Object. Działa dla nich ta sama zasada, czyli one także mają swój [[Prototype]], który wskazuje na Object.prototype, z którego mogą dziedziczyć jakieś funkcjonalności.


const ob = {};
Object.getPrototypeOf(ob) === Object.prototype //true

const tab = [1,2,3];
Object.getPrototypeOf(tab) === Array.prototype //true

typeof Array.prototype //"object"
Object.getPrototypeOf(Array.prototype) === Object.prototype //true

Można więc powiedzieć, że całe to prototypowe dziedziczenie przypomina swoistą siatkę zależności, w której kolejne obiekty są ze sobą połączone łańcuchem prototypów.

Mega siatka połączeń

Powyżej odnosiłem się do wbudowanych typów. My jako programiści możemy też oczywiście tworzyć nasze własne typy, które mają swoje własne metody i właściwości. Zajmiemy się tym w kolejnych rozdziałach.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.