Beim Programmieren wollen wir oft etwas nehmen und es erweitern.
Wir haben beispielsweise ein user
mit seinen Eigenschaften und Methoden und möchten admin
und guest
als leicht modifizierte Varianten davon erstellen. Wir möchten das, was wir in user
haben, wiederverwenden, seine Methoden nicht kopieren/neu implementieren, sondern einfach ein neues Objekt darauf erstellen.
Prototypische Vererbung ist eine Sprachfunktion, die dabei hilft.
In JavaScript haben Objekte eine spezielle versteckte Eigenschaft [[Prototype]]
(wie in der Spezifikation benannt), die entweder null
ist oder auf ein anderes Objekt verweist. Dieses Objekt wird „Prototyp“ genannt:
Wenn wir eine Eigenschaft aus object
lesen und diese fehlt, übernimmt JavaScript sie automatisch aus dem Prototyp. In der Programmierung nennt man das „prototypische Vererbung“. Und bald werden wir viele Beispiele einer solchen Vererbung sowie darauf aufbauende coolere Sprachfunktionen untersuchen.
Die Eigenschaft [[Prototype]]
ist intern und verborgen, es gibt jedoch viele Möglichkeiten, sie festzulegen.
Eine davon besteht darin, den speziellen Namen __proto__
zu verwenden, etwa so:
let animal = { isst: stimmt }; lass Kaninchen = { Sprünge: wahr }; Kaninchen.__proto__ = Tier; // setzt Kaninchen.[[Prototyp]] = Tier
Wenn wir nun eine Eigenschaft von rabbit
lesen und diese fehlt, übernimmt JavaScript sie automatisch von animal
.
Zum Beispiel:
let animal = { isst: stimmt }; lass Kaninchen = { Sprünge: wahr }; Kaninchen.__proto__ = Tier; // (*) // wir können jetzt beide Eigenschaften in Rabbit finden: alarm( Rabbit.eats ); // WAHR (**) alarm( Rabbit.jumps ); // WAHR
Hier setzt die Zeile (*)
animal
als Prototyp von rabbit
.
Wenn dann alert
versucht, die Eigenschaft rabbit.eats
(**)
zu lesen, ist sie nicht in rabbit
, also folgt JavaScript der [[Prototype]]
Referenz und findet sie in animal
(von unten nach oben):
Hier können wir sagen: „ animal
ist der Prototyp des rabbit
“ oder „ rabbit
erbt prototypisch vom animal
“.
Wenn animal
also über viele nützliche Eigenschaften und Methoden verfügt, stehen diese automatisch im rabbit
zur Verfügung. Solche Eigenschaften werden „vererbt“ genannt.
Wenn wir eine Methode in animal
haben, kann sie auf rabbit
aufgerufen werden:
let animal = { isst: wahr, gehen() { alarm("Animal walk"); } }; lass Kaninchen = { Sprünge: wahr, __proto__: Tier }; // walk ist vom Prototyp übernommen Rabbit.walk(); // Tierspaziergang
Die Methode wird automatisch aus dem Prototyp übernommen, etwa so:
Die Prototypenkette kann länger sein:
let animal = { isst: wahr, gehen() { alarm("Animal walk"); } }; lass Kaninchen = { Sprünge: wahr, __proto__: Tier }; sei longEar = { Ohrlänge: 10, __proto__: Kaninchen }; // walk wird aus der Prototypenkette übernommen longEar.walk(); // Tierspaziergang alarm(longEar.jumps); // wahr (von Kaninchen)
Wenn wir nun etwas aus longEar
lesen und es fehlt, sucht JavaScript in rabbit
und dann in animal
danach.
Es gibt nur zwei Einschränkungen:
Die Verweise dürfen sich nicht im Kreis drehen. JavaScript gibt einen Fehler aus, wenn wir versuchen, __proto__
in einem Kreis zuzuweisen.
Der Wert von __proto__
kann entweder ein Objekt oder null
sein. Andere Typen werden ignoriert.
Es mag auch offensichtlich sein, aber dennoch: Es kann nur einen [[Prototype]]
geben. Ein Objekt darf nicht von zwei anderen erben.
__proto__
ist ein historischer Getter/Setter für [[Prototype]]
Es ist ein häufiger Fehler von unerfahrenen Entwicklern, den Unterschied zwischen diesen beiden nicht zu kennen.
Bitte beachten Sie, dass __proto__
nicht mit der internen Eigenschaft [[Prototype]]
identisch ist. Es ist ein Getter/Setter für [[Prototype]]
. Später werden wir Situationen sehen, in denen es wichtig ist. Behalten wir es zunächst im Hinterkopf, während wir unser Verständnis der JavaScript-Sprache ausbauen.
Die Eigenschaft __proto__
ist etwas veraltet. Es existiert aus historischen Gründen. Modernes JavaScript schlägt vor, dass wir stattdessen die Funktionen Object.getPrototypeOf/Object.setPrototypeOf
verwenden sollten, die den Prototyp abrufen/festlegen. Wir werden diese Funktionen später auch behandeln.
Gemäß der Spezifikation darf __proto__
nur von Browsern unterstützt werden. Tatsächlich unterstützen jedoch alle Umgebungen, einschließlich der serverseitigen Umgebung, __proto__
, sodass wir bei der Verwendung ziemlich sicher sind.
Da die __proto__
Notation etwas intuitiver ist, verwenden wir sie in den Beispielen.
Der Prototyp wird nur zum Lesen von Eigenschaften verwendet.
Schreib-/Löschvorgänge funktionieren direkt mit dem Objekt.
Im folgenden Beispiel weisen wir rabbit
eine eigene walk
-Methode zu:
let animal = { isst: wahr, gehen() { /* Diese Methode wird von Rabbit nicht verwendet */ } }; lass Kaninchen = { __proto__: Tier }; Rabbit.walk = function() { alarm("Kaninchen! Bounce-bounce!"); }; Rabbit.walk(); // Kaninchen! Bounce-bounce!
Von nun an findet der Aufruf von rabbit.walk()
die Methode sofort im Objekt und führt sie aus, ohne den Prototyp zu verwenden:
Eine Ausnahme bilden Accessor-Eigenschaften, da die Zuweisung über eine Setter-Funktion erfolgt. Das Schreiben in eine solche Eigenschaft ist also eigentlich dasselbe wie das Aufrufen einer Funktion.
Aus diesem Grund funktioniert admin.fullName
im folgenden Code korrekt:
let user = { Name: „John“, Nachname: „Smith“, set fullName(value) { [dieser.Name, dieser.Nachname] = value.split(" "); }, get fullName() { return `${this.name} ${this.surname}`; } }; let admin = { __proto__: Benutzer, isAdmin: wahr }; Alert(admin.fullName); // John Smith (*) // Setter löst aus! admin.fullName = "Alice Cooper"; // (**) Alert(admin.fullName); // Alice Cooper, Status des Administrators geändert alarm(user.fullName); // John Smith, Status des Benutzers geschützt
Hier in der Zeile (*)
hat die Eigenschaft admin.fullName
einen Getter im Prototyp user
, daher wird er aufgerufen. Und in der Zeile (**)
hat die Eigenschaft einen Setter im Prototyp, daher wird sie aufgerufen.
Im obigen Beispiel könnte sich eine interessante Frage stellen: Welchen Wert hat this
Inside- set fullName(value)
? Wo werden die Eigenschaften this.name
und this.surname
geschrieben: in user
oder admin
?
Die Antwort ist einfach: this
wird durch Prototypen überhaupt nicht beeinflusst.
Egal wo die Methode zu finden ist: in einem Objekt oder seinem Prototyp. Bei einem Methodenaufruf ist this
immer das Objekt vor dem Punkt.
Der Setter-Aufruf admin.fullName=
verwendet also admin
als this
, nicht user
.
Das ist eigentlich eine überaus wichtige Sache, denn wir haben möglicherweise ein großes Objekt mit vielen Methoden und Objekte, die davon erben. Und wenn die erbenden Objekte die geerbten Methoden ausführen, ändern sie nur ihre eigenen Zustände, nicht den Zustand des großen Objekts.
Hier stellt beispielsweise animal
einen „Methodenspeicher“ dar, und rabbit
macht davon Gebrauch.
Der Aufruf rabbit.sleep()
legt this.isSleeping
auf dem rabbit
-Objekt fest:
// Tier hat Methoden let animal = { gehen() { if (!this.isSleeping) { alarm(`Ich gehe`); } }, schlafen() { this.isSleeping = true; } }; lass Kaninchen = { Name: „Weißes Kaninchen“, __proto__: Tier }; // ändert Rabbit.isSleeping Rabbit.sleep(); alarm(rabbit.isSleeping); // WAHR alarm(animal.isSleeping); // undefiniert (keine solche Eigenschaft im Prototyp)
Das resultierende Bild:
Wenn wir andere Objekte wie bird
, snake
usw. hätten, die von animal
erben würden, würden sie auch Zugang zu den Methoden von animal
erhalten. this
wäre jedoch bei jedem Methodenaufruf das entsprechende Objekt, das zum Zeitpunkt des Aufrufs (vor dem Punkt) ausgewertet wird, nicht animal
. Wenn wir this
Daten hineinschreiben, werden sie in diesen Objekten gespeichert.
Dadurch werden Methoden gemeinsam genutzt, der Objektstatus jedoch nicht.
Die for..in
Schleife durchläuft auch geerbte Eigenschaften.
Zum Beispiel:
let animal = { isst: stimmt }; lass Kaninchen = { Sprünge: wahr, __proto__: Tier }; // Object.keys gibt nur eigene Schlüssel zurück alarm(Object.keys(rabbit)); // springt // for..in durchläuft sowohl eigene als auch geerbte Schlüssel for(let prop in Rabbit) alarm(prop); // springt, dann isst
Wenn wir das nicht wollen und geerbte Eigenschaften ausschließen möchten, gibt es die integrierte Methode obj.hasOwnProperty(key): Sie gibt true
zurück, wenn obj
über eine eigene (nicht geerbte) Eigenschaft namens key
verfügt.
So können wir geerbte Eigenschaften herausfiltern (oder etwas anderes damit machen):
let animal = { isst: stimmt }; lass Kaninchen = { Sprünge: wahr, __proto__: Tier }; for(let prop in Rabbit) { let isOwn = Rabbit.hasOwnProperty(prop); if (isOwn) { alarm(`Our: ${prop}`); // Unser: Sprünge } anders { alarm(`Geerbt: ${prop}`); // Vererbt: isst } }
Hier haben wir die folgende Vererbungskette: rabbit
erbt von animal
, das von Object.prototype
erbt (da animal
ein Literalobjekt {...}
ist, also standardmäßig), und dann null
darüber:
Beachten Sie, es gibt eine lustige Sache. Woher kommt die Methode rabbit.hasOwnProperty
? Wir haben es nicht definiert. Wenn wir uns die Kette ansehen, können wir sehen, dass die Methode von Object.prototype.hasOwnProperty
bereitgestellt wird. Mit anderen Worten, es ist vererbt.
…Aber warum erscheint hasOwnProperty
nicht in der for..in
-Schleife wie eats
und jumps
, wenn for..in
geerbte Eigenschaften auflistet?
Die Antwort ist einfach: Es ist nicht aufzählbar. Wie alle anderen Eigenschaften von Object.prototype
verfügt es über das Flag enumerable:false
. Und for..in
listet nur aufzählbare Eigenschaften auf. Aus diesem Grund werden es und die übrigen Object.prototype
-Eigenschaften nicht aufgeführt.
Fast alle anderen Methoden zum Abrufen von Schlüsseln/Werten ignorieren geerbte Eigenschaften
Fast alle anderen Methoden zum Abrufen von Schlüsseln/Werten, wie z. B. Object.keys
, Object.values
usw., ignorieren geerbte Eigenschaften.
Sie wirken nur auf das Objekt selbst. Eigenschaften aus dem Prototyp werden nicht berücksichtigt.
In JavaScript haben alle Objekte eine versteckte [[Prototype]]
Eigenschaft, die entweder ein anderes Objekt oder null
ist.
Wir können obj.__proto__
verwenden, um darauf zuzugreifen (ein historischer Getter/Setter, es gibt auch andere Möglichkeiten, die bald behandelt werden).
Das von [[Prototype]]
referenzierte Objekt wird als „Prototyp“ bezeichnet.
Wenn wir eine Eigenschaft von obj
lesen oder eine Methode aufrufen möchten und diese nicht existiert, versucht JavaScript, sie im Prototyp zu finden.
Schreib-/Löschvorgänge wirken sich direkt auf das Objekt aus und verwenden nicht den Prototyp (vorausgesetzt, es handelt sich um eine Dateneigenschaft und nicht um einen Setter).
Wenn wir obj.method()
aufrufen und die method
vom Prototyp übernommen wird, verweist this
immer noch auf obj
. Daher funktionieren Methoden immer mit dem aktuellen Objekt, auch wenn sie geerbt sind.
Die for..in
Schleife durchläuft sowohl ihre eigenen als auch ihre geerbten Eigenschaften. Alle anderen Methoden zum Abrufen von Schlüsseln/Werten wirken sich nur auf das Objekt selbst aus.
Wichtigkeit: 5
Hier ist der Code, der ein Objektpaar erstellt und es dann ändert.
Welche Werte werden dabei angezeigt?
let animal = { Sprünge: null }; lass Kaninchen = { __proto__: Tier, Sprünge: wahr }; alarm( Rabbit.jumps ); // ? (1) Kaninchen.Jumps löschen; alarm( Rabbit.jumps ); // ? (2) animal.jumps löschen; alarm( Rabbit.jumps ); // ? (3)
Es sollten 3 Antworten vorhanden sein.
true
, vom rabbit
genommen.
null
, entnommen aus animal
.
undefined
, gibt es keine solche Eigenschaft mehr.
Wichtigkeit: 5
Die Aufgabe besteht aus zwei Teilen.
Angesichts der folgenden Objekte:
let head = { Gläser: 1 }; let table = { Stift: 3 }; Bett lassen = { Blatt: 1, Kissen: 2 }; lass Taschen = { Geld: 2000 };
Verwenden Sie __proto__
, um Prototypen so zuzuweisen, dass jede Eigenschaftssuche dem Pfad folgt: pockets
→ bed
→ table
→ head
. Beispielsweise sollte pockets.pen
3
haben (in table
enthalten) und bed.glasses
den Wert 1
(in head
enthalten).
Beantworten Sie die Frage: Ist es schneller, glasses
als pockets.glasses
oder head.glasses
zu bekommen? Benchmark bei Bedarf.
Fügen wir __proto__
hinzu:
let head = { Gläser: 1 }; let table = { Stift: 3, __proto__: Kopf }; schlafen lassen = { Blatt: 1, Kissen: 2, __proto__: Tabelle }; lass Taschen = { Geld: 2000, __proto__: Bett }; Alert(Taschen.pen); // 3 alarm( bed.glasses ); // 1 alarm( table.money ); // undefiniert
In modernen Engines macht es hinsichtlich der Leistung keinen Unterschied, ob wir eine Eigenschaft von einem Objekt oder seinem Prototyp übernehmen. Sie merken sich, wo die Immobilie gefunden wurde, und verwenden sie bei der nächsten Anfrage wieder.
Bei pockets.glasses
merken sie sich beispielsweise, wo sie glasses
gefunden haben (im head
) und werden beim nächsten Mal genau dort suchen. Sie sind außerdem intelligent genug, um interne Caches zu aktualisieren, wenn sich etwas ändert, sodass die Optimierung sicher ist.
Wichtigkeit: 5
Wir haben rabbit
, das von animal
erbt.
Wenn wir rabbit.eat()
aufrufen, welches Objekt erhält die full
Eigenschaft: animal
oder rabbit
?
let animal = { essen() { this.full = true; } }; lass Kaninchen = { __proto__: Tier }; Rabbit.eat();
Die Antwort: rabbit
.
Das liegt daran, dass es sich this
um ein Objekt vor dem Punkt handelt, sodass rabbit.eat()
rabbit
ändert.
Immobiliensuche und -ausführung sind zwei verschiedene Dinge.
Die Methode rabbit.eat
wird zunächst im Prototyp gefunden und dann mit this=rabbit
ausgeführt.
Wichtigkeit: 5
Wir haben zwei Hamster: speedy
und lazy
die vom allgemeinen hamster
erben.
Wenn wir einen von ihnen füttern, ist auch der andere satt. Warum? Wie können wir es beheben?
lass Hamster = { Magen: [], essen(Essen) { this.stomach.push(food); } }; lass schnell = { __proto__: Hamster }; lass faul = { __proto__: Hamster }; // Dieser hat das Essen gefunden speedy.eat("apple"); alarm( speedy.stomach ); // Apfel // Dieser hat es auch, warum? Bitte beheben. alarm( lazy.stomach ); // Apfel
Schauen wir uns genau an, was im Aufruf speedy.eat("apple")
vor sich geht.
Die Methode speedy.eat
wird im Prototyp ( =hamster
) gefunden und dann mit this=speedy
(dem Objekt vor dem Punkt) ausgeführt.
Dann muss this.stomach.push()
die stomach
finden und push
darauf aufrufen. Es wird this
nach stomach
gesucht ( =speedy
), aber nichts gefunden.
Dann folgt es der Prototypenkette und findet stomach
im hamster
.
Dann ruft es push
on it“ auf und fügt die Nahrung in den Magen des Prototyps ein.
Alle Hamster teilen sich also einen einzigen Magen!
Sowohl für lazy.stomach.push(...)
als auch speedy.stomach.push()
wird die Eigenschaft stomach
im Prototyp gefunden (da sie nicht im Objekt selbst enthalten ist), dann werden die neuen Daten hineingeschoben.
Bitte beachten Sie, dass so etwas bei einer einfachen Zuweisung this.stomach=
nicht passiert:
lass Hamster = { Magen: [], essen(Essen) { // this.stomach anstelle von this.stomach.push zuweisen this.stomach = [food]; } }; lass schnell = { __proto__: Hamster }; lass faul = { __proto__: Hamster }; // Schnell hat man das Essen gefunden speedy.eat("apple"); alarm( speedy.stomach ); // Apfel // Der Magen des Faulen ist leer alarm( lazy.stomach ); // <nichts>
Jetzt funktioniert alles einwandfrei, da this.stomach=
keine Suche nach stomach
durchführt. Der Wert wird direkt in this
Objekt geschrieben.
Wir können das Problem auch völlig vermeiden, indem wir dafür sorgen, dass jeder Hamster seinen eigenen Magen hat:
lass Hamster = { Magen: [], essen(Essen) { this.stomach.push(food); } }; lass schnell = { __proto__: Hamster, Magen: [] }; lass faul = { __proto__: Hamster, Magen: [] }; // Schnell hat man das Essen gefunden speedy.eat("apple"); alarm( speedy.stomach ); // Apfel // Der Magen des Faulen ist leer alarm( lazy.stomach ); // <nichts>
Als gängige Lösung sollten alle Eigenschaften, die den Zustand eines bestimmten Objekts beschreiben, wie oben stomach
, in dieses Objekt geschrieben werden. Das verhindert solche Probleme.