Prototypální dědičnost

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í:

  1. Reference nemohou být v kruzích. Pokud se pokusíme přiřadit __proto__, JavaScript vyvolá chybu v kruhu.
  2. Hodnota __proto__ může být buď objekt, nebo null . 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íče/hodnoty ignorují zděděné vlastnosti

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, nebo null .
  • 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() a method je převzato z prototypu, this stále odkazuje na obj . 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.