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]]
zobj
. - Object.setPrototypeOf(obj, proto) – nastaví
[[Prototype]]
zobj
naproto
.
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
aObject.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/setterobj.__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));
- doslovná syntaxe:
-
Moderní metody pro získání/nastavení prototypu jsou:
- Object.getPrototypeOf(obj) – vrátí
[[Prototype]]
zobj
(stejné jako__proto__
getter). - Object.setPrototypeOf(obj, proto) – nastaví
[[Prototype]]
zobj
naproto
(stejné jako__proto__
setr).
- Object.getPrototypeOf(obj) – vrátí
-
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 zObject.prototype
, čímž jsou odpovídající klíče „obsazeny“ a mohou způsobit vedlejší účinky. Snull
prototyp, objekty jsou skutečně prázdné.