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í.
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.
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é.
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í.
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.
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
.
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!
Ktorý objekt bude prototypom objektu závisí na tom, ako bol objekt vytvorený.
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
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.
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
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
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.
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.