Im ersten Kapitel dieses Abschnitts haben wir erwähnt, dass es moderne Methoden zum Aufbau eines Prototyps gibt.
Das Setzen oder Lesen des Prototyps mit obj.__proto__
gilt als veraltet und etwas veraltet (verschoben in den sogenannten „Annex B“ des JavaScript-Standards, der nur für Browser gedacht ist).
Die modernen Methoden zum Erhalten/Setzen eines Prototyps sind:
Object.getPrototypeOf(obj) – gibt den [[Prototype]]
von obj
zurück.
Object.setPrototypeOf(obj, proto) – setzt den [[Prototype]]
von obj
auf proto
.
Die einzige Verwendung von __proto__
, die nicht verpönt ist, ist als Eigenschaft beim Erstellen eines neuen Objekts: { __proto__: ... }
.
Allerdings gibt es auch hierfür eine spezielle Methode:
Object.create(proto[, descriptors]) – erstellt ein leeres Objekt mit dem angegebenen proto
als [[Prototype]]
und optionalen Eigenschaftsdeskriptoren.
Zum Beispiel:
let animal = { isst: stimmt }; // ein neues Objekt mit Tier als Prototyp erstellen let Rabbit = Object.create(animal); // dasselbe wie {__proto__: animal} alarm(rabbit.eats); // WAHR alarm(Object.getPrototypeOf(rabbit) === animal); // WAHR Object.setPrototypeOf(rabbit, {}); // den Prototyp von Rabbit in {} ändern
Die Object.create
-Methode ist etwas leistungsfähiger, da sie über ein optionales zweites Argument verfügt: Eigenschaftsdeskriptoren.
Dort können wir dem neuen Objekt zusätzliche Eigenschaften zur Verfügung stellen, etwa so:
let animal = { isst: stimmt }; let Rabbit = Object.create(animal, { Sprünge: { Wert: wahr } }); alarm(rabbit.jumps); // WAHR
Die Deskriptoren haben das gleiche Format wie im Kapitel Eigenschaftsflags und Deskriptoren beschrieben.
Wir können Object.create
verwenden, um ein Objektklonen durchzuführen, das leistungsfähiger ist als das Kopieren von Eigenschaften in for..in
:
let clone = Object.create( Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj) );
Dieser Aufruf erstellt eine wirklich exakte Kopie von obj
, einschließlich aller Eigenschaften: aufzählbar und nicht aufzählbar, Dateneigenschaften und Setter/Getter – alles und mit dem richtigen [[Prototype]]
.
Es gibt so viele Möglichkeiten [[Prototype]]
zu verwalten. Wie ist das passiert? Warum?
Das hat historische Gründe.
Das prototypische Erbe war von Anfang an in der Sprache vorhanden, aber die Art und Weise, es zu verwalten, entwickelte sich im Laufe der Zeit weiter.
Die prototype
einer Konstruktorfunktion funktioniert seit sehr langer Zeit. Dies ist die älteste Methode, Objekte mit einem bestimmten Prototyp zu erstellen.
Später, im Jahr 2012, erschien Object.create
im Standard. Es gab die Möglichkeit, Objekte mit einem bestimmten Prototyp zu erstellen, bot jedoch nicht die Möglichkeit, ihn abzurufen/festzulegen. Einige Browser haben den nicht standardmäßigen __proto__
Accessor implementiert, der es dem Benutzer ermöglicht, jederzeit einen Prototyp abzurufen/einzustellen, um Entwicklern mehr Flexibilität zu bieten.
Später, im Jahr 2015, wurden Object.setPrototypeOf
und Object.getPrototypeOf
zum Standard hinzugefügt, um die gleiche Funktionalität wie __proto__
auszuführen. Da __proto__
de facto überall implementiert war, war es gewissermaßen veraltet und gelangte in den Anhang B des Standards, das heißt: optional für Nicht-Browser-Umgebungen.
Später, im Jahr 2022, war es offiziell erlaubt, __proto__
in Objektliteralen {...}
zu verwenden (aus Anhang B entfernt), jedoch nicht als Getter/Setter obj.__proto__
(immer noch in Anhang B).
Warum wurde __proto__
durch die Funktionen getPrototypeOf/setPrototypeOf
ersetzt?
Warum wurde __proto__
teilweise rehabilitiert und seine Verwendung in {...}
erlaubt, jedoch nicht als Getter/Setter?
Das ist eine interessante Frage, bei der wir verstehen müssen, warum __proto__
schlecht ist.
Und bald bekommen wir die Antwort.
Ändern Sie [[Prototype]]
nicht an vorhandenen Objekten, wenn es auf Geschwindigkeit ankommt
Technisch gesehen können wir jederzeit [[Prototype]]
erhalten/einstellen. Aber normalerweise legen wir es nur einmal zum Zeitpunkt der Objekterstellung fest und ändern es nicht mehr: rabbit
erbt von animal
, und das wird sich nicht ändern.
Und JavaScript-Engines sind dafür stark optimiert. Das „on-the-fly“-Ändern eines Prototyps mit Object.setPrototypeOf
oder obj.__proto__=
ist ein sehr langsamer Vorgang, da dadurch interne Optimierungen für Zugriffsvorgänge auf Objekteigenschaften unterbrochen werden. Vermeiden Sie es also, es sei denn, Sie wissen, was Sie tun, oder die JavaScript-Geschwindigkeit spielt für Sie keine Rolle.
Wie wir wissen, können Objekte als assoziative Arrays zum Speichern von Schlüssel-/Wertpaaren verwendet werden.
…Aber wenn wir versuchen , vom Benutzer bereitgestellte Schlüssel darin zu speichern (z. B. ein vom Benutzer eingegebenes Wörterbuch), können wir einen interessanten Fehler feststellen: Alle Schlüssel funktionieren einwandfrei, außer "__proto__"
.
Schauen Sie sich das Beispiel an:
sei obj = {}; let key = prompt("Was ist der Schlüssel?", "__proto__"); obj[key] = „irgendein Wert“; alarm(obj[key]); // [Objekt Objekt], nicht „irgendein Wert“!
Wenn der Benutzer hier __proto__
eingibt, wird die Zuweisung in Zeile 4 ignoriert!
Für einen Nicht-Entwickler könnte das sicherlich überraschend sein, für uns ist das aber durchaus verständlich. Die Eigenschaft __proto__
ist etwas Besonderes: Sie muss entweder ein Objekt oder null
sein. Ein String kann kein Prototyp werden. Aus diesem Grund wird die Zuweisung eines Strings an __proto__
ignoriert.
Aber wir hatten doch nicht vor , ein solches Verhalten umzusetzen, oder? Wir möchten Schlüssel/Wert-Paare speichern und der Schlüssel mit dem Namen "__proto__"
wurde nicht ordnungsgemäß gespeichert. Das ist also ein Bug!
Hier sind die Folgen nicht schlimm. Aber in anderen Fällen speichern wir möglicherweise Objekte anstelle von Strings in obj
und dann wird der Prototyp tatsächlich geändert. Infolgedessen wird die Ausführung auf völlig unerwartete Weise schiefgehen.
Was noch schlimmer ist: Normalerweise denken Entwickler überhaupt nicht über eine solche Möglichkeit nach. Das macht solche Fehler schwer zu erkennen und kann sie sogar zu Schwachstellen machen, insbesondere wenn JavaScript auf der Serverseite verwendet wird.
Bei der Zuweisung zu obj.toString
können auch unerwartete Dinge passieren, da es sich um eine integrierte Objektmethode handelt.
Wie können wir dieses Problem vermeiden?
Zuerst können wir einfach dazu wechseln, Map
statt einfacher Objekte für die Speicherung zu verwenden, dann ist alles in Ordnung:
let map = new Map(); let key = prompt("Was ist der Schlüssel?", "__proto__"); map.set(key, „irgendein Wert“); alarm(map.get(key)); // „einiger Wert“ (wie beabsichtigt)
…Aber Object
ist oft ansprechender, da sie prägnanter ist.
Glücklicherweise können wir Objekte verwenden, denn Sprachentwickler haben sich schon vor langer Zeit mit diesem Problem beschäftigt.
Wie wir wissen, ist __proto__
keine Eigenschaft eines Objekts, sondern eine Accessoreigenschaft von Object.prototype
:
Wenn also obj.__proto__
gelesen oder gesetzt wird, wird der entsprechende Getter/Setter von seinem Prototyp aufgerufen und er ruft/setzt [[Prototype]]
.
Wie am Anfang dieses Tutorialabschnitts gesagt wurde: __proto__
ist eine Möglichkeit, auf [[Prototype]]
zuzugreifen, es ist nicht [[Prototype]]
selbst.
Wenn wir nun beabsichtigen, ein Objekt als assoziatives Array zu verwenden und solche Probleme zu vermeiden, können wir dies mit einem kleinen Trick tun:
let obj = Object.create(null); // oder: obj = { __proto__: null } let key = prompt("Was ist der Schlüssel?", "__proto__"); obj[key] = „irgendein Wert“; alarm(obj[key]); // „etwas Wert“
Object.create(null)
erstellt ein leeres Objekt ohne Prototyp ( [[Prototype]]
ist null
):
Es gibt also keinen geerbten Getter/Setter für __proto__
. Jetzt wird es als reguläre Dateneigenschaft verarbeitet, sodass das obige Beispiel ordnungsgemäß funktioniert.
Wir können solche Objekte „sehr einfache“ oder „reine Wörterbuch“-Objekte nennen, weil sie noch einfacher sind als das reguläre einfache Objekt {...}
.
Ein Nachteil ist, dass solchen Objekten keine integrierten Objektmethoden fehlen, z. B. toString
:
let obj = Object.create(null); alarm(obj); // Fehler (kein toString)
…Aber das ist normalerweise für assoziative Arrays in Ordnung.
Beachten Sie, dass die meisten objektbezogenen Methoden Object.something(...)
sind, wie Object.keys(obj)
– sie sind nicht im Prototyp enthalten, sodass sie weiterhin an solchen Objekten arbeiten:
let chineseDictionary = Object.create(null); chineseDictionary.hello = "你好"; chineseDictionary.bye = "再见"; alarm(Object.keys(chineseDictionary)); // Hallo, tschüss
Um ein Objekt mit dem angegebenen Prototyp zu erstellen, verwenden Sie:
Object.create
bietet eine einfache Möglichkeit, ein Objekt mit allen Deskriptoren flach zu kopieren:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
Literale Syntax: { __proto__: ... }
, ermöglicht die Angabe mehrerer Eigenschaften
oder Object.create(proto[, descriptors]), ermöglicht die Angabe von Eigenschaftsdeskriptoren.
Moderne Methoden zum Erhalten/Setzen des Prototyps sind:
Object.getPrototypeOf(obj) – gibt den [[Prototype]]
von obj
zurück (dasselbe wie __proto__
Getter).
Object.setPrototypeOf(obj, proto) – setzt den [[Prototype]]
von obj
auf proto
(dasselbe wie __proto__
setter).
Das Abrufen/Festlegen des Prototyps mit dem integrierten __proto__
Getter/Setter wird nicht empfohlen, es befindet sich jetzt im Anhang B der Spezifikation.
Wir haben auch prototyplose Objekte behandelt, die mit Object.create(null)
oder {__proto__: null}
erstellt wurden.
Diese Objekte werden als Wörterbücher verwendet, um beliebige (möglicherweise vom Benutzer generierte) Schlüssel zu speichern.
Normalerweise erben Objekte integrierte Methoden und __proto__
Getter/Setter von Object.prototype
, wodurch entsprechende Schlüssel „belegt“ werden und möglicherweise Nebenwirkungen verursachen. Bei null
-Prototyp sind Objekte wirklich leer.
Wichtigkeit: 5
Es gibt ein dictionary
, das als Object.create(null)
erstellt wurde, um beliebige key/value
-Paare zu speichern.
Fügen Sie die Methode dictionary.toString()
hinzu, die eine durch Kommas getrennte Liste von Schlüsseln zurückgeben sollte. Ihr toString
sollte nicht in for..in
über dem Objekt angezeigt werden.
So sollte es funktionieren:
let dictionary = Object.create(null); // Ihr Code zum Hinzufügen der dictionary.toString-Methode // einige Daten hinzufügen dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // __proto__ ist hier ein regulärer Eigenschaftsschlüssel // Nur Apple und __proto__ sind in der Schleife for(Eingabe im Wörterbuch) { alarm(key); // „apple“, dann „__proto__“ } // Ihr toString in Aktion Alert(Wörterbuch); // „apple,__proto__“
Die Methode kann alle aufzählbaren Schlüssel mithilfe von Object.keys
übernehmen und deren Liste ausgeben.
Um toString
nicht aufzählbar zu machen, definieren wir es mithilfe eines Eigenschaftsdeskriptors. Die Syntax von Object.create
ermöglicht es uns, ein Objekt mit Eigenschaftsdeskriptoren als zweites Argument bereitzustellen.
let dictionary = Object.create(null, { toString: { // toString-Eigenschaft definieren value() { // Der Wert ist eine Funktion return Object.keys(this).join(); } } }); dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // Apple und __proto__ sind in der Schleife for(Eingabe im Wörterbuch) { alarm(key); // „apple“, dann „__proto__“ } // durch Kommas getrennte Liste von Eigenschaften nach toString Alert(Wörterbuch); // „apple,__proto__“
Wenn wir eine Eigenschaft mithilfe eines Deskriptors erstellen, sind ihre Flags standardmäßig false
. Im obigen Code ist dictionary.toString
also nicht aufzählbar.
Weitere Informationen finden Sie im Kapitel Eigenschaftsflags und Deskriptoren.
Wichtigkeit: 5
Lassen Sie uns ein neues rabbit
erstellen:
Funktion Kaninchen(Name) { this.name = Name; } Rabbit.prototype.sayHi = function() { alarm(this.name); }; let Rabbit = new Rabbit("Rabbit");
Diese Anrufe bewirken dasselbe oder nicht?
Rabbit.sayHi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(rabbit).sayHi(); Rabbit.__proto__.sayHi();
Der erste Aufruf hat this == rabbit
, die anderen haben this
gleich Rabbit.prototype
, weil es tatsächlich das Objekt vor dem Punkt ist.
Also zeigt nur der erste Aufruf Rabbit
, andere zeigen undefined
:
Funktion Kaninchen(Name) { this.name = Name; } Rabbit.prototype.sayHi = function() { alarm( this.name ); } let Rabbit = new Rabbit("Rabbit"); Rabbit.sayHi(); // Kaninchen Rabbit.prototype.sayHi(); // undefiniert Object.getPrototypeOf(rabbit).sayHi(); // undefiniert Rabbit.__proto__.sayHi(); // undefiniert