V programování často chceme něco vzít a rozšířit to.
Například máme user
objekt s jeho vlastnostmi a metodami a chcete vytvořit admin
a guest
jako jeho mírně upravené varianty. Rádi bychom znovu použili to, co máme v user
, nekopírovat/reimplementovat jeho metody, pouze na něj postavit nový objekt.
Prototypální dědičnost je jazyková funkce, která v tom pomáhá.
[[Prototype]]
V JavaScriptu mají objekty speciální skrytou vlastnost [[Prototype]]
(jak je uvedeno ve specifikaci), tedy buď null
nebo odkazuje na jiný objekt. Tento objekt se nazývá „prototyp“:
Když čteme vlastnost z object
a chybí, JavaScript jej automaticky přebírá z prototypu. V programování se tomu říká „prototypová dědičnost“. A brzy budeme studovat mnoho příkladů takové dědičnosti, stejně jako chladnější jazykové funkce na ní postavené.
Vlastnost [[Prototype]]
je interní a skrytý, ale existuje mnoho způsobů, jak jej nastavit.
Jedním z nich je použití speciálního názvu __proto__
, takto:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal
Nyní, když čteme vlastnost z rabbit
, a chybí, JavaScript jej automaticky převezme z animal
.
Například:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
Zde je řádek (*)
nastaví animal
být prototypem rabbit
.
Poté, když alert
pokusí se přečíst vlastnost rabbit.eats
(**)
, není v rabbit
, takže JavaScript následuje [[Prototype]]
odkaz a najde jej v animal
(podívejte se zdola nahoru):
Zde můžeme říci, že "animal
je prototypem rabbit
“ nebo „rabbit
prototypicky dědí z animal
".
Pokud tedy animal
má mnoho užitečných vlastností a metod, pak jsou automaticky dostupné v rabbit
. Takové vlastnosti se nazývají „zděděné“.
Pokud máme metodu v animal
, lze jej volat na rabbit
:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk is taken from the prototype
rabbit.walk(); // Animal walk
Metoda je automaticky převzata z prototypu, takto:
Řetězec prototypu může být delší:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)
Nyní, když čteme něco z longEar
, a chybí, JavaScript jej vyhledá v rabbit
a poté v animal
.
Existují pouze dvě omezení:
- Reference nemohou být v kruzích. Pokud se pokusíme přiřadit
__proto__
, JavaScript vyvolá chybu v kruhu. - Hodnota
__proto__
může být buď objekt, nebonull
. Ostatní typy jsou ignorovány.
Také to může být zřejmé, ale přesto:může být pouze jeden [[Prototype]]
. Objekt nesmí dědit od dvou dalších.
__proto__
je historický getter/setter pro [[Prototype]]
Je běžnou chybou začínajících vývojářů, že neznají rozdíl mezi těmito dvěma.
Vezměte prosím na vědomí, že __proto__
není totéž jako interní [[Prototype]]
vlastnictví. Je to getter/setter pro [[Prototype]]
. Později uvidíme situace, kdy na tom záleží, zatím to mějme na paměti, až si budeme budovat znalosti jazyka JavaScript.
__proto__
nemovitost je trochu zastaralá. Existuje z historických důvodů, moderní JavaScript naznačuje, že bychom měli používat Object.getPrototypeOf/Object.setPrototypeOf
funkce, které získávají/nastavují prototyp. Těmto funkcím se také budeme věnovat později.
Podle specifikace __proto__
musí být podporovány pouze prohlížeči. Ve skutečnosti však všechna prostředí včetně serveru podporují __proto__
, takže jeho používání je zcela bezpečné.
Jako __proto__
zápis je o něco intuitivnější, používáme ho v příkladech.
Psaní nepoužívá prototyp
Prototyp se používá pouze pro čtení vlastností.
Operace zápisu/mazání pracují přímo s objektem.
V níže uvedeném příkladu mu přiřadíme vlastní walk
metoda na rabbit
:
let animal = {
eats: true,
walk() {
/* this method won't be used by rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
Od této chvíle rabbit.walk()
call najde metodu okamžitě v objektu a provede ji, aniž by použil prototyp:
Vlastnosti přístupového objektu jsou výjimkou, protože přiřazení je řešeno funkcí setter. Zápis do takové vlastnosti je tedy vlastně stejný jako volání funkce.
Z toho důvodu admin.fullName
funguje správně v níže uvedeném kódu:
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper, state of admin modified
alert(user.fullName); // John Smith, state of user protected
Zde na řádku (*)
vlastnost admin.fullName
má getter v prototypu user
, tak se tomu říká. A v řádku (**)
vlastnost má v prototypu setter, takže se nazývá.
Hodnota „toto“
Ve výše uvedeném příkladu může vyvstat zajímavá otázka:jaká je hodnota this
uvnitř set fullName(value)
? Kde jsou vlastnosti this.name
a this.surname
zapsáno:do user
nebo admin
?
Odpověď je jednoduchá:this
není prototypy vůbec ovlivněna.
Bez ohledu na to, kde se metoda nachází:v objektu nebo jeho prototypu. Ve volání metody this
je vždy objekt před tečkou.
Setter tedy zavolá admin.fullName=
používá admin
jako this
, nikoli user
.
To je ve skutečnosti velmi důležitá věc, protože můžeme mít velký objekt s mnoha metodami a mít objekty, které z něj dědí. A když dědící objekty spustí zděděné metody, upraví pouze své vlastní stavy, nikoli stav velkého objektu.
Například zde animal
představuje „úložiště metod“ a rabbit
využívá to.
Volání rabbit.sleep()
nastaví this.isSleeping
na rabbit
objekt:
// animal has methods
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// modifies rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)
Výsledný obrázek:
Kdybychom měli další objekty, například bird
, snake
, atd., dědí z animal
, také by získali přístup k metodám animal
. Ale this
v každém volání metody by byl odpovídající objekt vyhodnocený v době volání (před tečkou), nikoli animal
. Když tedy zapisujeme data do this
, ukládá se do těchto objektů.
V důsledku toho jsou metody sdíleny, ale stav objektu nikoli.
pro...ve smyčce
for..in
smyčka iteruje i přes zděděné vlastnosti.
Například:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys only returns own keys
alert(Object.keys(rabbit)); // jumps
// for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then eats
Pokud to není to, co chceme, a chtěli bychom vyloučit zděděné vlastnosti, existuje vestavěná metoda obj.hasOwnProperty(key):vrací true
pokud obj
má svou vlastní (ne zděděnou) vlastnost s názvem key
.
Můžeme tedy odfiltrovat zděděné vlastnosti (nebo s nimi udělat něco jiného):
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
Zde máme následující řetězec dědičnosti:rabbit
dědí z animal
, který dědí z Object.prototype
(protože animal
je doslovný objekt {...}
, takže je to ve výchozím nastavení) a poté null
nad ním:
Všimněte si, je tu jedna legrační věc. Kde je metoda rabbit.hasOwnProperty
přicházející z? My jsme to nedefinovali. Při pohledu na řetězec vidíme, že metodu poskytuje Object.prototype.hasOwnProperty
. Jinými slovy, je zděděno.
…Ale proč hasOwnProperty
neobjeví se v for..in
smyčka jako eats
a jumps
udělat, pokud for..in
uvádí zděděné vlastnosti?
Odpověď je jednoduchá:není to vyčíslitelné. Stejně jako všechny ostatní vlastnosti Object.prototype
, má enumerable:false
vlajka. A for..in
uvádí pouze vyčíslitelné vlastnosti. To je důvod, proč on a zbytek Object.prototype
vlastnosti nejsou uvedeny.
Téměř všechny ostatní metody získávání klíčů a hodnot, jako je Object.keys
, Object.values
a tak dále ignorovat zděděné vlastnosti.
Fungují pouze na samotném objektu. Vlastnosti z prototypu nejsou vzít v úvahu.
Shrnutí
- V JavaScriptu mají všechny objekty skrytý
[[Prototype]]
vlastnost, která je buď jiným objektem, nebonull
. - Můžeme použít
obj.__proto__
k přístupu k němu (historický getter/setter, existují i jiné způsoby, které budou brzy pokryty). - Objekt, na který odkazuje
[[Prototype]]
se nazývá „prototyp“. - Pokud chceme číst vlastnost
obj
nebo zavolejte metodu a ta neexistuje, pak se ji JavaScript pokusí najít v prototypu. - Operace zápisu/mazání působí přímo na objekt, nepoužívají prototyp (za předpokladu, že se jedná o datovou vlastnost, nikoli setter).
- Pokud zavoláme
obj.method()
amethod
je převzato z prototypu,this
stále odkazuje naobj
. Metody tedy vždy pracují s aktuálním objektem, i když jsou zděděny. for..in
smyčka iteruje přes své vlastní i své zděděné vlastnosti. Všechny ostatní metody získávání klíče/hodnoty fungují pouze na samotném objektu.