Dziedziczenie prototypowe
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 się wzięły te wszystkie funkcjonalności?
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).
Obiekty w Javascript
Na podobnej zasadzie działają obiekty w Javascript. Gdy tworzysz jakiś obiekt jakiegoś typu (np. Array, String, Number), możesz dla niego odpalać różne funkcjonalności, które są dziedziczone.
W Javascript występuje tak zwane dziedziczenie prototypowe. Oznacza to, że każdy obiekt dziedziczy właściwości i metody z innego obiektu - zwanego tutaj prototypem.
Sprawdźmy to na prostym 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]]
.
Jest to referencja dodawana przez JavaScript praktycznie każdemu obiektowi. Wskazuje ona na inny obiekt, który jest prototypem danego obiektu, a z którego nasz obiekt może dziedziczyć funkcjonalności.
Wskazanie na prototyp obiektu może być wyświetlane w twojej przeglądarce 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 xxx", w Firefoxie na Ubuntu to "prototype", natomiast w wielu nowych przeglądarkach zobaczysz go 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ę 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()
.
Spójrzmy na kolejny przykład:
const cat = {
name : "Konczenti"
}
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 - za pomocą referencji przejdzie do prototypu danego obiektu i tam spróbuje użyć danej właściwości.
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]]
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" 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 jaki stosowaliśmy dotychczas), ale też za pomocą tak zwanych konstruktorów.
const obA = {} //literal
const obB = new Object(); //klasa 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];
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
Prototypy danych typów 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 szaleństwo przypomina swoistą siatkę zależności, w której kolejne obiekty są ze sobą połączone łańcuchem prototypów.
Powyżej skupiliśmy się głównie na wbudowanych typach. My jako programiści możemy też oczywiście tworzyć nasze własne typy, które mają swoje metody i właściwości. Zajmiemy się tym w kolejnych rozdziałach.
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania