PÅi programovánà Äasto chceme nÄco vzÃt a rozÅ¡ÃÅit.
NapÅÃklad máme objekt uživatel s jeho vlastnostmi a metodami a chceme vytvoÅit objekty správce a host jako jeho mÃrnÄ pozmÄnÄné varianty. Rádi bychom použili to, co máme v objektu uživatel, nekopÃrovali ani znovu neimplementovali jeho metody, jen postavili nový objekt na jeho základu.
Prvek jazyka, který nám s tÃm pomůže, je prototypová dÄdiÄnost.
[[Prototype]]
V JavaScriptu majà objekty speciálnà skrytou vlastnost [[Prototype]] (jak je nazvána ve specifikaci), která je buÄ null, nebo se odkazuje na jiný objekt. Tento objekt se nazývá âprototypâ:
Když naÄÃtáme z objektu vlastnost, která v nÄm nenÃ, JavaScript ji automaticky pÅevezme z prototypu. V programovánà se to nazývá âprototypová dÄdiÄnostâ. Brzy prostudujeme mnoho pÅÃkladů této dÄdiÄnosti i chytÅejšà prvky jazyka, které jsou na nà postaveny.
Vlastnost [[Prototype]] je internà a skrytá, ale je mnoho způsobů, jak ji nastavit.
JednÃm z nich je použÃt speciálnà název __proto__, napÅÃklad takto:
let zvÃÅe = {
žere: true
};
let králÃk = {
skáÄe: true
};
králÃk.__proto__ = zvÃÅe; // nastavà králÃk.[[Prototype]] = zvÃÅe
Když nynà budeme naÄÃtat nÄjakou vlastnost z objektu králÃk a ta v nÄm nebude, JavaScript ji automaticky pÅevezme z objektu zvÃÅe.
NapÅÃklad:
let zvÃÅe = {
žere: true
};
let králÃk = {
skáÄe: true
};
králÃk.__proto__ = zvÃÅe; // (*)
// nynà můžeme v králÃkovi najÃt obÄ vlastnosti:
alert( králÃk.žere ); // true (**)
alert( králÃk.skáÄe ); // true
Zde se na Åádku (*) nastavà zvÃÅe jako prototyp objektu králÃk.
Když se pak alert pokusà naÄÃst vlastnost králÃk.žere (**), v objektu králÃk tato vlastnost nenÃ, takže JavaScript následuje odkaz [[Prototype]] a najde ji v objektu zvÃÅe (dÃvá se zdola nahoru):
Zde můžeme ÅÃci, že âobjekt zvÃÅe je prototyp objektu králÃkâ nebo že âobjekt králÃk je prototypovÄ zdÄdÄn z objektu zvÃÅeâ.
Má-li tedy zvÃÅe množstvà užiteÄných vlastnostà a metod, pak se stanou automaticky dostupnými i v objektu králÃk. Takové vlastnosti se nazývajà âzdÄdÄnéâ.
Máme-li metodu v objektu zvÃÅe, můžeme ji volat na objektu králÃk:
let zvÃÅe = {
žere: true,
jdi() {
alert("ZvÃÅe jde");
}
};
let králÃk = {
skáÄe: true,
__proto__: zvÃÅe
};
// jdi se pÅevezme z prototypu
králÃk.jdi(); // ZvÃÅe jde
Tato metoda se automaticky pÅevezme z prototypu tÃmto způsobem:
ÅetÄzec prototypů může být delÅ¡Ã:
let zvÃÅe = {
žere: true,
jdi() {
alert("ZvÃÅe jde");
}
};
let králÃk = {
skáÄe: true,
__proto__: zvÃÅe
};
let ušatý = {
délkaUcha: 10,
__proto__: králÃk
};
// jdi se pÅevezme z ÅetÄzce prototypů
uÅ¡atý.jdi(); // ZvÃÅe jde
alert(uÅ¡atý.skáÄe); // true (z králÃka)
Když nynà budeme nÄco naÄÃtat z objektu uÅ¡atý a nebude to v nÄm, JavaScript to bude hledat v objektu králÃk a pak v objektu zvÃÅe.
Omezenà jsou jen dvÄ:
- Odkazy nemohou tvoÅit kruh. Pokud se pokusÃme nastavit
__proto__kruhovÄ, JavaScript ohlásà chybu. - Hodnota
__proto__může být buÄ objekt, nebonull. Jiné typy jsou ignorovány.
Možná je to zÅejmé, ale pÅesto: [[Prototype]] může být pouze jeden. Objekt nemůže být zdÄdÄn ze dvou jiných objektů.
__proto__ je historický getter/setter pro [[Prototype]]Je bÄžnou chybou zaÄÃnajÃcÃch vývojáÅů neznat rozdÃl mezi nimi.
ProsÃme vÅ¡imnÄte si, že __proto__ nenà totéž jako internà vlastnost [[Prototype]], ale je to getter/setter vlastnosti [[Prototype]]. PozdÄji uvidÃme situace, v nichž to hraje roli. ProzatÃm to jen mÄjme na pamÄti, když se snažÃme porozumÄt jazyku JavaScript.
Vlastnost __proto__ je ponÄkud zastaralá. Existuje z historických důvodů, modernà JavaScript naznaÄuje, že bychom mÃsto Ätenà a nastavovánà prototypu mÄli použÃvat funkce Object.getPrototypeOf/Object.setPrototypeOf. Tyto funkce rovnÄž probereme pozdÄji.
Specifikace vyžaduje podporu __proto__ jen od prohlÞeÄů. Ve skuteÄnosti vÅ¡ak podporujà __proto__ vÅ¡echna prostÅedà vÄetnÄ tÄch na stranÄ serveru, takže jeho použÃvánà je pomÄrnÄ bezpeÄné.
Protože notace __proto__ je intuitivnÄ trochu jasnÄjÅ¡Ã, budeme ji použÃvat v pÅÃkladech.
PÅi zápisu se prototyp nepoužÃvá
Prototyp se použÃvá jen pÅi Ätenà vlastnostÃ.
Operace zápisu a mazánà pracujà pÅÃmo s objektem.
V následujÃcÃm pÅÃkladu pÅiÅadÃme objektu králÃk jeho vlastnà metodu jdi:
let zvÃÅe = {
žere: true,
jdi() {
/* tuto metodu králÃk nepoužije */
}
};
let králÃk = {
__proto__: zvÃÅe
};
králÃk.jdi = function() {
alert("KrálÃk! Hop hop hop!");
};
králÃk.jdi(); // KrálÃk! Hop hop hop!
Od nynÄjÅ¡ka volánà králÃk.jdi() najde metodu rovnou v objektu a spustà ji bez použità prototypu:
Výjimkou jsou pÅÃstupové vlastnosti, jelikož pÅiÅazenà je zpracováváno setterem. Zápis do takové vlastnosti je tedy ve skuteÄnosti totéž jako volánà funkce.
Z tohoto důvodu bude v následujÃcÃm kódu korektnÄ fungovat správce.celéJméno:
let uživatel = {
jméno: "Jan",
pÅÃjmenÃ: "Novák",
set celéJméno(hodnota) {
[this.jméno, this.pÅÃjmenÃ] = hodnota.split(" ");
},
get celéJméno() {
return `${this.jméno} ${this.pÅÃjmenÃ}`;
}
};
let správce = {
__proto__: uživatel,
jesprávce: true
};
alert(správce.celéJméno); // Jan Novák (*)
// spustà se setter!
správce.celéJméno = "Alice Cooper"; // (**)
alert(správce.celéJméno); // Alice Cooper, stav správcea modifikován
alert(uživatel.celéJméno); // Jan Novák, stav uživatele ochránÄn
Zde na Åádku (*) má vlastnost správce.celéJméno getter v prototypu uživatel, takže se zavolá tento getter. A na Åádku (**) má tato vlastnost v tomto prototypu setter, takže se zavolá tento setter.
Hodnota âthisâ
V uvedeném pÅÃkladu se může objevit zajÃmavá otázka: jaká je hodnota this uvnitÅ set celéJméno(hodnota)? Kam se zapÚà vlastnosti this.jméno a this.pÅÃjmenÃ: do objektu uživatel, nebo správce?
OdpovÄÄ je jednoduchá: this nenà prototypem vůbec ovlivnÄno.
Nezáležà na tom, kde je metoda nalezena: zda v objektu nebo v prototypu. PÅi volánà metody je this vždy objekt pÅed teÄkou.
Volánà setteru správce.celéJméno= tedy jako this použije objekt správce, ne uživatel.
To je ve skuteÄnosti nesmÃrnÄ důležitá vÄc, neboÅ¥ můžeme mÃt velký objekt obsahujÃcà mnoho metod a objekty, které z nÄj budou dÄdit. A když budou dÄdÃcà objekty spouÅ¡tÄt zdÄdÄné metody, budou modifikovat jen svůj vlastnà stav a ne stav onoho velkého objektu.
NapÅÃklad zde zvÃÅe pÅedstavuje âsklad metodâ a králÃk jej využÃvá.
Volánà králÃk.spi() nastavà this.jeSpÃcà na objektu králÃk:
// zvÃÅe má metody
let zvÃÅe = {
jdi() {
if (!this.jeSpÃcÃ) {
alert(`Já jdu`);
}
},
spi() {
this.jeSpÃcà = true;
}
};
let králÃk = {
jméno: "BÃlý králÃk",
__proto__: zvÃÅe
};
// modifikuje králÃk.jeSpÃcÃ
králÃk.spi();
alert(králÃk.jeSpÃcÃ); // true
alert(zvÃÅe.jeSpÃcÃ); // undefined (v prototypu žádná taková vlastnost nenÃ)
Výsledný obrázek:
Kdybychom mÄli jiné objekty, které dÄdà ze zvÃÅe, napÅ. pták, had atd., tyto objekty by také mÄly pÅÃstup k metodám objektu zvÃÅe. AvÅ¡ak this by pÅi každém volánà metody byl pÅÃsluÅ¡ný objekt, vyhodnocený v Äase volánà (pÅed teÄkou), ne zvÃÅe. Když tedy zapÃÅ¡eme data do this, uložà se do tÄchto objektů.
Výsledkem je, že metody jsou sdÃlené, ale stav objektu ne.
Cyklus forâ¦in
Cyklus for..in iteruje i nad zdÄdÄnými vlastnostmi.
NapÅÃklad:
let zvÃÅe = {
žere: true
};
let králÃk = {
skáÄe: true,
__proto__: zvÃÅe
};
// Object.keys vrátà jen jeho vlastnà klÃÄe
alert(Object.keys(králÃk)); // skáÄe
// for..in cykluje nad vlastnÃmi i zdÄdÄnými klÃÄi
for(let vlastnost in králÃk) alert(vlastnost); // skáÄe, pak žere
Pokud to tak nechceme a rádi bychom zdÄdÄné vlastnosti vylouÄili, existuje zabudovaná metoda obj.hasOwnProperty(klÃÄ): vrátà true, jestliže obj má svou vlastnà (ne zdÄdÄnou) vlastnost jménem klÃÄ.
Můžeme tedy zdÄdÄné vlastnosti odfiltrovat (nebo s nimi udÄlat nÄco jiného):
let zvÃÅe = {
žere: true
};
let králÃk = {
skáÄe: true,
__proto__: zvÃÅe
};
for(let vlastnost in králÃk) {
let jeVlastnà = králÃk.hasOwnProperty(vlastnost);
if (jeVlastnÃ) {
alert(`NaÅ¡e: ${vlastnost}`); // NaÅ¡e: skáÄe
} else {
alert(`ZdÄdÄná: ${vlastnost}`); // ZdÄdÄná: žere
}
}
Zde máme následujÃcà ÅetÄzec dÄdiÄnosti: králÃk dÄdà ze zvÃÅe, to dÄdà z Object.prototype (to je standardnÃ, protože zvÃÅe je literálnà objekt {...}) a pak z null nad nÃm:
VÅ¡imnÄte si jedné zábavné vÄci. Odkud pocházà metoda králÃk.hasOwnProperty? Tu jsme pÅece nedefinovali. Když se podÃváme na ÅetÄzec, uvidÃme, že tuto metodu poskytuje Object.prototype.hasOwnProperty. Jinými slovy, je zdÄdÄná.
â¦ProÄ se vÅ¡ak hasOwnProperty neobjevà v cyklu for..in jako metody žere a skáÄe, když for..in vyjmenovává zdÄdÄné vlastnosti?
OdpovÄÄ je jednoduchá: nenà enumerovatelná. StejnÄ jako vÅ¡echny ostatnà vlastnosti z Object.prototype má pÅepÃnaÄ enumerable:false. A for..in vyjmenovává jedinÄ enumerovatelné vlastnosti. To je důvod, proÄ nevyjmenuje ji ani ostatnà vlastnosti z Object.prototype.
TémÄÅ vÅ¡echny ostatnà metody pro zÃskávánà klÃÄů a hodnot, napÅÃklad Object.keys, Object.values a tak dále, zdÄdÄné vlastnosti ignorujÃ.
Pracujà pouze nad samotným objektem. Vlastnosti z prototypu se neberou v úvahu.
ShrnutÃ
- V JavaScriptu majà všechny vlastnosti skrytou vlastnost
[[Prototype]], která je buÄ jiný objekt, nebonull. - K pÅÃstupu k nà můžeme použÃt
obj.__proto__(je to historický getter/setter, existujà i jiné způsoby, které brzy probereme). - Objekt, na který odkazuje
[[Prototype]], se nazývá âprototypâ. - Chceme-li naÄÃst vlastnost nebo volat metodu objektu
obja ta neexistuje, pak se ji JavaScript pokusà najÃt v prototypu. - Operace zápisu a mazánà pracujà pÅÃmo nad objektem a nepoužÃvajà prototyp (za pÅedpokladu, že je to datová vlastnost a ne setter).
- Voláme-li
obj.metoda()ametodaje pÅevzata z prototypu, pakthisstále odkazuje naobj. Metody tedy vždy pracujà s aktuálnÃm objektem, i když jsou zdÄdÄné. - Cyklus
for..initeruje jak nad vlastnÃmi, tak nad zdÄdÄnými vlastnostmi. VÅ¡echny ostatnà metody pro zÃskávánà klÃÄů a hodnot pracujà jen nad samotným objektem.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)