Praktický JavaScript (4.) - Prototypová dedičnosť

Publikoval Michal Kočí dňa 16.9.2018 o 19:00 v kategórii JavaScript

V JavaScripte neexistuje tradičná objektová dedičnosť, akú poznáte napríklad z jazyku C# či iných. Dedičnosť v JavaScripte ale existuje, volá sa Prototypová Dedičnosť a dnes si ju prakticky vysvetlíme, pretože každý, kto to s JavaScriptom myslí vážne, jej rozumieť jednoducho musí.

Prototyp

Každý objekt v JavaScripte má vnútornú väzbu na iný objekt, na svoj prototyp. Je to jeho vlastnosť, ktorá je obvykle interná a vy sa tak na ňu teoreticky neviete pozrieť. Pokiaľ však rozumiete jednoduchým pravidlám, tak viete, kto je prototypom toho ktorého objektu.

Prakticky, vďaka tomu, že pravidlám rozumiete, respektíve budete rozumieť, nemáte obvykle ani dôvod sa programovo pozerať na to, kto tým prototypom je. Toto je informácia pre JavaScriptový runtime, ktorý túto informáciu potrebuje a sám ju používa.

Na čo teda prototyp slúži? Slúži na to, že objekt z neho dedí vlastnosti a metódy. Ale nepredbiehajme.

Ako sa pozrieť na prototyp?

Zvedavý čitateľ v tomto momente určite chce vedieť a hlavne vidieť na vlastné oči, kto je prototyp objektu. Nejakého objektu.

OK, keď inak nedáte. V niektorých runtimoch, ale nie vo všetkých, máte šancu sa na prototyp dostať cez neštandardnú vlastnosť __proto__:

let o = { name: 'Michal' };

console.log(o.__proto__);
// {}

Lepší spôsob potom je použiť funkciu Object.getPrototypeOf(obj), ktorá je súčasťou štandardu ES od verzie 5.1:

let o = { name: 'Michal' };

console.log(Object.getPrototypeOf(o));
// {}

Fajn, zvedavý čitateľ je v tomto momente zrejme uspokojený, poďme sa ale pozrieť na prototyp a dedičnosť, aby sme vedeli, na čo je nám to vlastne dobré.

Prototypová reťaz (Prototype Chain)

Objekt má teda väzbu na svoj prototyp. A tento prototyp je, áno, správne, objekt. A keďže je to objekt, aj ten môže mať väzbu na svoj prototyp. Čo bude zase objekt. Vidíte už, kam mierim?

Vzniká nám tak niečo, čo sa volá prototypová reťaz (Prototype chain). Po ktorej viete ísť smerom nahor. Až prídeme na koniec tejto reťaze. Ten sa spozná tak, že niektorý objekt v tejto reťazi ju ukončuje. Ukončuje ju tak, že ako svoj prototyp má hodnotu null.

Reťaz môže byť rôzne dlhá a vždy niekde končí. Vždy na jej konci je objekt, ktorý už prototyp nemá. Ako je dlhá závisí na tom, ako boli objekty vytvorené. Ale pravidlám sa budeme venovať neskôr.

Pozrime sa teda, či nám aj naša reťaz z predchádzajúceho príkladu niekde končí:

let o = { name: 'Michal' };

console.log(Object.getPrototypeOf(o));
// {}

console.log(Object.getPrototypeOf(Object.getPrototypeOf(o)));
// null

Končí. Náš objekt má prototyp - objekt. A tento objekt, náš prototyp, už ďalší, svoj vlastný, nemá. Naša prototypová reťaz tak nie je moc dlhá, ale to vôbec nevadí.

Vlastnosti objektu a prístup k nim

Keď chcete pristúpiť k vlastnosti objektu v JavaScripte, máte v zásade dve možnosti. Bodkovú alebo hranato-zátvorkovú notáciu. Pozrime sa na ne.

Prvou je bodková notácia:

let o = { name: 'Michal' };

console.log(o.name);
// Michal

Druhou je prístup k vlastnosti cez hranaté zátvorky:

let o = { name: 'Michal' };

console.log(o['name']);
// Michal

Oba prístupy sú si rovnocenné, ten druhý sa vám hodí viac v prípade, keď názov vlastnosti obsahuje napríklad medzeru, alebo ho máte v premennej. Ale o tom sa nechceme teraz baviť.

Čo sa teda deje, keď pristupujete k nejakej vlastnosti objektu? Nuž, runtime ju musí nájsť.

V našom prípade je to jednoduché, lebo vlastnosť name sa nachádza priamo na objekte. Hovoríme o tzv. vlastnej vlastnosti (own property). Čo sa ale deje v nasledovnom prípade?

let o = { name: 'Michal' };

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

Náš objekt predsa nemá vlastnosť (nezabudnite, aj funkcia je "len" vlastnosť) toString, tak ako je možné, že do konzoly bolo niečo zapísané? Ako to, že tam nebolo vypísané undefined prípadne že program nezlyhal?

Nuž odpoveďou je: možné to je práve vďaka prototypovej dedičnosti.

Ako funguje prototypová dedičnosť

Už sme si povedali, že keď pristupujete k vlastnosti objektu, runtime ju musí nájsť a naznačili sme, že ju hľadá na danom objekte, medzi jeho vlastnými vlastnosťami.

A úlohou prototypovej dedičnosti je, že pokiaľ sa vlastnosť nenachádza na objekte, runtime zistí, kto je jeho prototyp a pozrie sa, či ten náhodou vlastnosť s daným menom neobsahuje.

Ak ju neobsahuje ani ten, t.j. prototyp objektu, runtime sa opäť pozrie, či má tento prototyp svoj prototyp a skúsi hľadať na ňom.

Čiže, vlastnosť sa vždy hľadá priamo na objekte, potom na jeho prototype, potom na prototype prototypu, potom na prototype prototypu prototypu, atď...

Ide sa jednoducho po prototypovej reťazi smerom nahor. Keď je vlastnosť na niektorej úrovni nájdená, je výsledkom výrazu tá. A ak sa v celej prototypovej reťazi vlastnosť nenájde, výsledkom je hodnota undefined.

Ako sa viete pozrieť na vlastnú vlastnosť

Opäť, pre zvedavých čitateľov tu mám malú odbočku. Ako sa viete pozrieť na to, či, či má objekt vlastnú vlastnosť s daným názvom? Použijete funkciu hasOwnProperty:

let o = { name: 'Michal' };

console.log(o.hasOwnProperty('name'));
// true

A teraz na chvíľu zastavme a zamyslime sa. Fajn, máme objekt o s jednou jedinou vlastnou vlastnosťou name. Vieme to, lebo sme ho tak vytvorili.

Takže, opäť podobná otázka ako vyššie. Ako je možné, že do konzoly bolo zapísané true, kde sa vzala funkcia hasOwnProperty?

Nuž, isto tušíte, že sa nachádza na niektorom z prototypov v prototypovej reťazi. Výborne!

Kto je prototypom objektu

Ktorý objekt bude prototypom objektu závisí na tom, ako bol objekt vytvorený.

Objekt vytvorený cez object/array literal

Keď vytvárate objekt cez object literal, t.j. {}, ako napríklad na už použitom prípade:

let o = { name: 'Michal' };

bude jeho prototypom objekt, ktorý sa nachádza na vlastnosti prototype objektu Object. Čiže prototyp bude ukazovať na Object.prototype. To si ľahko overíme:

let o = { name: 'Michal' };

console.log(Object.getPrototypeOf(o) === Object.prototype);
// true

No a to je zároveň objekt, ktorý má na sebe funkcie, ktorých použitie sme už videli, ako napríklad toString, hasOwnProperty a iné.

console.log(Object.prototype.hasOwnProperty('hasOwnProperty'));
// true

console.log(Object.prototype.hasOwnProperty('toString'));
// true

Objekt vytvorený cez array literal

Obdobne to funguje, ak použijeme array literal, t.j. [], na vytvorenie poľa. V tom prípade bude prototyp nastavený na Array.prototype:

let a = [1, 2, 3];

console.log(Object.getPrototypeOf(a) === Array.prototype);
// true

Vďaka tomu máte na každom poli dostupné funkcie ako map, slice, filter a podobne:

console.log(Array.prototype.hasOwnProperty('map'));
// true

console.log(Array.prototype.hasOwnProperty('slice'));
// true

No a jeho prototyp je zase nastavený na Object.prototype, vďaka čomu máte zase prístup k vlastnostiam ako hasOwnProperty a ďalším.

Objekt vytvorený cez konštruktor

Od EcmaScriptu 6 máme k dispozícii možnosť vytvárať triedy cez kľúčové slovo class, jedná sa ale o ekvivalent toho, čo sme dovtedy poznali pod konštruktorom.

Pripomeňme si, čo je konštruktor v JavaScripte - je to funkcia, ktorú keď zavoláme s klúčovým slovom new, vytvorí sa nám inštancia, čiže objekt.

Mohli by sme si teda vytvoriť konštruktor pre triedu Person, tá bude prijímať cez konštruktor meno a to nastaví do vlastnej vlastnosti:

function Person(name) {
    this.name = name;
}

Konštruktor teda máme, vytvorme si inštanciu tejto triedy:

let p = new Person('Michal);

console.log(p.name);
// Michal

Ako to bude s prototypom takéhoto objektu? Pokiaľ čítate celý tento článok pozorne, potom už asi tušíte. Áno, bude ním objekt Person.prototype.

Len na pripomenutie, každá funkcia v JavaScripte je zároveň aj objektom. No a každý JavaScriptový objekt na sebe môže mať ľubovoľné vlastnosti.

Každá funkcia má na sebe vlastnosť prototype a táto vlastnosť je použitá ako prototyp pre všetky inštancie vytvorené zavolaním tejto funkcie ako konštruktora.

Overme si teda, že prototyp našej inštancie naozaj je nastavený na Person.prototype. Prekvapením isto nebude ani to, že prototypom objektu Person.prototype je opäť náš starý kamarát, Object.prototype:

let p = new Person('Michal);

console.log(Object.getPrototypeOf(p) === Person.prototype);
// true

console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);
// true

Objekt vytvorený s explicitne nastaveným prototypom

Posledný prípad, ktorý vám chcem ukázať je taký, kedy si chceme vytvoriť prázdny objekt, ale s tým, že chceme explicitne povedať, kto má byť jeho prototypom.

To vieme spraviť použitím metódy create objektu Object. Funkcia Object.create(prototype) prijíma objekt a vracia nový, prázdny objekt.

let proto = { isPerson: true };
let obj = Object.create(proto);

console.log(obj.isPerson);
// true

Vrátený objekt bude mať nastavený prototyp na nami použitý argument funkcie:

let proto = { isPerson: true };
let obj = Object.create(proto);

console.log(Object.getPrototypeOf(obj) === proto);
// true

Na čo je dobrý tento scenár? V zásade na dve veci. Prvou je, ak chcete vytvoriť úplne čistý objekt (žiadne vlastnosti) navyše bez prototypu. Stačí poslať ako hodnotu parametra hodnotu null:

let clear = Object.create(null);

console.log(Object.getPrototypeOf(clear) === null)

Druhý scenár je, keď chcete mať ako prototyp nejaký užitočný objekt, ktorý má napríklad sadu funkcií a potom chcete vytvárať objekty, ktoré na neho budú odkazovať. Je to taký iný, ale v JavaScripte povolený a niekedy používaný prístup:

let proto = {
    greet: function() { console.log('Hello ' + this.name) }
};

let person = Object.create(proto);
person.name = 'Michal';

person.greet();
// Hello Michal

Záver

Pokiaľ ste dočítali až sem, tak by ste mali rozumieť pojmom ako sú prototyp a prototypová dedičnosť, ale hlavne by ste mali chápať tomuto dôležitému konceptu JavaScriptu.

Vždy je jednoducho dobré ovládať jazyk a jeho detaily dokonale. Nuž a nabudúce sa skúsime pozrieť opäť na niečo užitočné, takže ostaňte naladení...

Chcete sa poriadne naučiť JavaScript a ešte sa pritom naučiť nejaký framework či knižnicu (napríklad Angular či React)? Prídite na niektoré z mojich JavaScriptových školení a naučte sa to rýchlo a jednoducho.

Mohlo by ťa tiež zaujímať

Páčil sa ti príspevok?

Zdieľaj príspevok alebo si ho odlož na neskôr

Sleduj ma

Ak nechceš premeškať príspevky ako je tento, sleduj ma na Twitteri, alebo ak máš RSS čítačku, môžeš sledovať môj RSS kanál.