Prototypové metody, objekty bez __proto__

V první kapitole této části jsme zmínili, že existují moderní metody pro nastavení prototypu.

Nastavení nebo čtení prototypu pomocí obj.__proto__ je považováno za zastaralé a poněkud zastaralé (přesunuto do tzv. „Přílohy B“ standardu JavaScript, určené pouze pro prohlížeče).

Moderní metody pro získání/nastavení prototypu jsou:

  • Object.getPrototypeOf(obj) – vrátí [[Prototype]] z obj .
  • Object.setPrototypeOf(obj, proto) – nastaví [[Prototype]] z obj na proto .

Jediné použití __proto__ , která není odsuzována, je jako vlastnost při vytváření nového objektu:{ __proto__: ... } .

I když na to existuje speciální metoda:

  • Object.create(proto, [deskriptory]) – vytvoří prázdný objekt s daným proto jako [[Prototype]] a volitelné popisovače vlastností.

Například:

let animal = {
 eats: true
};

// create a new object with animal as a prototype
let rabbit = Object.create(animal); // same as {__proto__: animal}

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}

Object.create metoda je o něco výkonnější, protože má volitelný druhý argument:deskriptory vlastností.

Zde můžeme novému objektu poskytnout další vlastnosti, například:

let animal = {
 eats: true
};

let rabbit = Object.create(animal, {
 jumps: {
 value: true
 }
});

alert(rabbit.jumps); // true

Deskriptory jsou ve stejném formátu, jaký je popsán v kapitole Parametry a deskriptory vlastností.

Můžeme použít Object.create k provedení klonování objektů, které je výkonnější než kopírování vlastností v for..in :

let clone = Object.create(
 Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)
);

Toto volání vytváří skutečně přesnou kopii obj , včetně všech vlastností:vyčíslitelné a nesčíslitelné, vlastnosti dat a nastavovače/gettery – vše a se správným [[Prototype]] .

Stručná historie

Existuje mnoho způsobů, jak spravovat [[Prototype]] . Jak se to stalo? Proč?

Je to z historických důvodů.

Prototypové dědictví bylo v jazyce od jeho úsvitu, ale způsoby, jak jej spravovat, se postupem času vyvíjely.

  • prototype vlastnost funkce konstruktoru funguje již od pradávna. Je to nejstarší způsob, jak vytvořit objekty s daným prototypem.
  • Později, v roce 2012, Object.create se objevil ve standardu. Dával možnost vytvářet objekty s daným prototypem, ale neposkytoval možnost jej získat/nastavit. Některé prohlížeče implementovaly nestandardní __proto__ přístupový prvek, který uživateli umožňoval získat/nastavit prototyp kdykoli, a poskytnout tak vývojářům větší flexibilitu.
  • Později, v roce 2015, Object.setPrototypeOf a Object.getPrototypeOf byly přidány do standardu, aby vykonávaly stejné funkce jako __proto__ . Jako __proto__ byl de-facto implementován všude, byl tak trochu zastaralý a dostal se do přílohy B standardu, to znamená:volitelné pro prostředí bez prohlížeče.
  • Později, v roce 2022, bylo oficiálně povoleno používat __proto__ v objektových literálech {...} (přesunuto z přílohy B), ale ne jako getter/setter obj.__proto__ (stále v příloze B).

Proč bylo __proto__ nahrazeny funkcemi getPrototypeOf/setPrototypeOf ?

Proč bylo __proto__ částečně rehabilitován a jeho použití povoleno v {...} , ale ne jako getter/setter?

To je zajímavá otázka, která vyžaduje, abychom pochopili, proč __proto__ je špatný.

A brzy dostaneme odpověď.

Neměňte [[Prototype]] na stávající objekty, pokud na rychlosti záleží

Technicky můžeme získat/nastavit [[Prototype]] kdykoliv. Obvykle jej však nastavíme pouze jednou při vytváření objektu a již jej neupravujeme:rabbit dědí z animal , a to se nezmění.

A JavaScriptové enginy jsou pro to vysoce optimalizované. Změna prototypu „za běhu“ pomocí Object.setPrototypeOf nebo obj.__proto__= je velmi pomalá operace, protože narušuje interní optimalizace pro operace přístupu k vlastnostem objektu. Vyhněte se tomu, pokud nevíte, co děláte, nebo pokud pro vás na rychlosti JavaScriptu nezáleží.

Objekty "Velmi jednoduché"

Jak víme, objekty lze použít jako asociativní pole k uložení párů klíč/hodnota.

…Ale pokud se pokusíme uložit poskytnuté uživatelem klíče v něm (například slovník zadaný uživatelem), můžeme vidět zajímavou závadu:všechny klíče fungují dobře kromě "__proto__" .

Podívejte se na příklad:

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object], not "some value"!

Zde, pokud uživatel zadá __proto__ , přiřazení na řádku 4 je ignorováno!

To by mohlo být jistě překvapivé pro nevývojáře, ale pro nás docela pochopitelné. __proto__ vlastnost je speciální:musí to být buď objekt nebo null . Řetězec se nemůže stát prototypem. Proto přiřaďte řetězec __proto__ je ignorováno.

Ale to jsme neměli v úmyslu implementovat takové chování, že? Chceme uložit páry klíč/hodnota a klíč s názvem "__proto__" nebyl správně uložen. Tak to je chyba!

Zde následky nejsou hrozné. Ale v jiných případech můžeme ukládat objekty místo řetězců v obj a pak bude prototyp skutečně změněn. V důsledku toho se provedení pokazí zcela neočekávaným způsobem.

A co hůř – vývojáři většinou o takové možnosti vůbec neuvažují. Díky tomu je obtížné si takové chyby všimnout a dokonce je proměnit ve zranitelnost, zvláště když se na straně serveru používá JavaScript.

Při přiřazování k obj.toString se také mohou stát neočekávané věci , protože je to vestavěná objektová metoda.

Jak se můžeme tomuto problému vyhnout?

Za prvé, můžeme přejít na používání Map pro ukládání místo obyčejných objektů, pak je vše v pořádku:

let map = new Map();

let key = prompt("What's the key?", "__proto__");
map.set(key, "some value");

alert(map.get(key)); // "some value" (as intended)

…Ale Object syntaxe je často přitažlivější, protože je stručnější.

Naštěstí můžeme používat objekty, protože tvůrci jazyka na tento problém mysleli již dávno.

Jak víme, __proto__ není vlastnost objektu, ale vlastnost přístupového objektu Object.prototype :

Pokud tedy obj.__proto__ je načten nebo nastaven, odpovídající getter/setter je volán z jeho prototypu a získá/nastaví [[Prototype]] .

Jak bylo řečeno na začátku této části tutoriálu:__proto__ je způsob přístupu k [[Prototype]] , není to [[Prototype]] sám.

Nyní, pokud máme v úmyslu použít objekt jako asociativní pole a vyhnout se takovým problémům, můžeme to udělat pomocí malého triku:

let obj = Object.create(null);
// or: obj = { __proto__: null }

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

Object.create(null) vytvoří prázdný objekt bez prototypu ([[Prototype]] je null ):

Pro __proto__ tedy neexistuje žádný zděděný getter/setter . Nyní se zpracovává jako běžná datová vlastnost, takže výše uvedený příklad funguje správně.

Takové objekty můžeme nazvat „velmi plain“ nebo „pure dictionary“ objekty, protože jsou ještě jednodušší než běžný plain objekt {...} .

Nevýhodou je, že takové objekty postrádají jakékoli vestavěné objektové metody, např. toString :

let obj = Object.create(null);

alert(obj); // Error (no toString)

…Ale to je obvykle v pořádku pro asociativní pole.

Všimněte si, že většina metod souvisejících s objektem je Object.something(...) , například Object.keys(obj) – nejsou v prototypu, takže na takových objektech budou dál pracovat:

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";

alert(Object.keys(chineseDictionary)); // hello,bye

Shrnutí

  • Chcete-li vytvořit objekt s daným prototypem, použijte:

    • doslovná syntaxe:{ __proto__: ... } , umožňuje zadat více vlastností
    • nebo Object.create(proto, [deskriptory]), umožňuje zadat deskriptory vlastností.

    Object.create poskytuje snadný způsob, jak povrchně zkopírovat objekt se všemi deskriptory:

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
  • Moderní metody pro získání/nastavení prototypu jsou:

    • Object.getPrototypeOf(obj) – vrátí [[Prototype]] z obj (stejné jako __proto__ getter).
    • Object.setPrototypeOf(obj, proto) – nastaví [[Prototype]] z obj na proto (stejné jako __proto__ setr).
  • Získání/nastavení prototypu pomocí vestavěného __proto__ getter/setter se nedoporučuje, je nyní v příloze B specifikace.

  • Pokryli jsme také objekty bez prototypů vytvořené pomocí Object.create(null) nebo {__proto__: null} .

    Tyto objekty se používají jako slovníky k ukládání jakýchkoli (případně uživatelem generovaných) klíčů.

    Normálně objekty dědí vestavěné metody a __proto__ getter/setter z Object.prototype , čímž jsou odpovídající klíče „obsazeny“ a mohou způsobit vedlejší účinky. S null prototyp, objekty jsou skutečně prázdné.