JavaScript Prototypale Vererbung
JavaScript prototypale Vererbung: [[Prototype]]-Link, __proto__ vs Object.getPrototypeOf/setPrototypeOf, Prototypkette, F.prototype, Property-Shadowing und this — mit ausführbaren Beispielen.
In JavaScript können Objekte Eigenschaften und Methoden von anderen Objekten erben — durch einen Mechanismus namens prototypale Vererbung. Anstatt Funktionen aus einer Klasse zu kopieren (wie in Java oder C++), hält jedes object einen versteckten Link zu einem anderen object — seinem Prototyp — und JavaScript folgt diesem Link, wenn eine Eigenschaft am object selbst nicht gefunden wird. Diese Seite erklärt, wie dieser versteckte Link funktioniert, den Unterschied zwischen __proto__ und Object.getPrototypeOf/setPrototypeOf, wie die Prototypkette durchsucht wird, die Rolle von F.prototype bei Konstruktorfunktionen, Property-Shadowing sowie das Verhalten von this entlang der Kette.
Der versteckte [[Prototype]]-Link
Jedes JavaScript-object hat eine versteckte interne Eigenschaft namens [[Prototype]]. Sie ist entweder null oder eine Referenz auf ein anderes object, und dieses referenzierte object wird als Prototyp des Objekts bezeichnet.
[[Prototype]] ist ein interner Slot der Sprachspezifikation — er lässt sich nicht mit einem normalen Eigenschaftszugriff auslesen. Stattdessen arbeitet man darüber mit zwei öffentlichen APIs:
- Dem historischen Accessor
__proto__(ein Getter/Setter, der vonObject.prototypebereitgestellt wird). - Den modernen, empfohlenen Methoden
Object.getPrototypeOf(obj)undObject.setPrototypeOf(obj, proto).
Der einfachste Weg, einen Prototyp zu setzen, ist __proto__ innerhalb eines object-Literals. Hier machen wir animal zum Prototyp von rabbit, sodass rabbit die Eigenschaften von animal lesen kann:
__proto__ vs Object.getPrototypeOf / setPrototypeOf
__proto__ ist ein Getter/Setter, der für den allgemeinen Einsatz seit langem als veraltet gilt — er ist nur aus Gründen der Browser-Kompatibilität standardisiert. Im produktiven Code sollte man die expliziten Methoden bevorzugen:
Beachte den Unterschied: __proto__ ist ein Eigenschafts-Accessor, während getPrototypeOf/setPrototypeOf Funktionen sind. Zwei praktische Regeln:
__proto__ist nicht dasselbe wie[[Prototype]].__proto__ist lediglich der Accessor, der den internen[[Prototype]]-Slot liest und schreibt.- Vermeide es, einen Prototyp nach der Erstellung eines Objekts zu ändern.
Object.setPrototypeOfund das Zuweisen vonobj.__proto__sind langsame Operationen: Engines optimieren Objekte, deren Prototyp zum Erstellungszeitpunkt feststeht, erheblich. Setze den Prototyp einmalig beim Aufbau des Objekts.
Die Prototypkette
Der Prototyp eines Prototyps kann selbst wiederum einen Prototyp haben und so eine Prototypkette bilden. Wenn obj.prop gelesen wird, geht JavaScript folgendermaßen vor:
- Sucht
propals eigene Eigenschaft vonobj. - Wenn nicht gefunden, folgt es
[[Prototype]]und sucht auf dem Prototyp. - Wiederholt Schritt 2 entlang der Kette, bis
propgefunden wird odernullerreicht ist.
Endet die Kette bei null, ohne die Eigenschaft zu finden, ist das Ergebnis undefined.
Für die Kette gelten zwei Einschränkungen: Referenzen dürfen keine Schleife bilden (JavaScript wirft einen Fehler, wenn man versucht, einen Zyklus zu erzeugen), und [[Prototype]] muss entweder ein object oder null sein.
Property-Shadowing: Schreiben vs. Lesen
Der Prototyp wird nur beim Lesen konsultiert. Beim Schreiben oder Löschen einer Eigenschaft wirkt die Operation immer auf das object selbst, niemals auf seinen Prototyp. Wird eine Eigenschaft zugewiesen, die auch auf dem Prototyp existiert, entsteht eine eigene Eigenschaft, die die geerbte überlagert (verbirgt):
Die Ausnahme bilden Accessor-Eigenschaften (Getter/Setter): Da ein Setter ein Funktionsaufruf ist, führt das Schreiben über einen geerbten Setter diesen Setter aus, anstatt eine neue eigene Dateneigenschaft zu erzeugen.
this ist immer das aufrufende object
Eine häufige Fehlerquelle: Egal wo sich eine Methode in der Kette befindet, this darin ist das object vor dem Punkt zum Zeitpunkt des Aufrufs — niemals der Prototyp, auf dem sie definiert wurde. Geerbte Methoden arbeiten daher mit dem eigenen Zustand des erbenden Objekts:
Das macht gemeinsam genutzte Methoden auf einem Prototyp so nützlich: eine einzige Methodendefinition, aber jedes object speichert seine eigenen Daten.
Konstruktorfunktionen und F.prototype
[[Prototype]] für jedes object manuell zu setzen ist mühsam. Das klassische Muster ist eine Konstruktorfunktion in Verbindung mit new. Jede Funktion besitzt eine reguläre Eigenschaft namens prototype (geschrieben als F.prototype). Wenn new F() aufgerufen wird, wird [[Prototype]] des neu erstellten Objekts auf F.prototype gesetzt.
F.prototype ist klar von [[Prototype]] eines Objekts zu unterscheiden: F.prototype ist eine gewöhnliche Eigenschaft der Konstruktorfunktion, die [[Prototype]] für Objekte liefert, die mit new F() erzeugt wurden. Standardmäßig ist F.prototype ein object mit einer einzigen nicht-aufzählbaren constructor-Eigenschaft, die auf die Funktion selbst zeigt.
Hinweis: Die moderne ES6-
class-Syntax ist syntaktischer Zucker über genau diesen Mechanismus. Eineclass-Deklaration erstellt eine Konstruktorfunktion, legt ihre Methoden aufConstructor.prototypeab und verbindet die Kette mit denselben[[Prototype]]-Links. Siehe JavaScript Class Inheritance für dieextends/super-Form.
Objekte mit Object.create erstellen
Object.create(proto) erstellt ein neues object, dessen [[Prototype]] direkt auf proto gesetzt wird — ohne Konstruktor. Es ist die expliziteste Methode zur Einrichtung von Vererbung und akzeptiert ein zweites Argument: eine Property-Descriptor-Map im selben Format wie Object.defineProperties.
Die Descriptor-Map erlaubt es, Flags wie writable, enumerable und configurable zu steuern — unter JavaScript Property Flags and Descriptors ist erklärt, was jedes Flag bewirkt. Objekte, die mit Object.create(null) erstellt wurden (sogenannte „sehr einfache" Objekte), werden unter Prototype Methods, Objects Without __proto__ behandelt.
Mehrstufige Vererbung
Da jeder Prototyp wiederum einen eigenen Prototyp haben kann, lassen sich mehrere Ebenen tiefe Ketten aufbauen, um spezifischere Beziehungen abzubilden:
Die Kette untersuchen und iterieren
Um zu prüfen, ob ein object irgendwo in der Kette eines anderen Objekts vorkommt, verwendet man isPrototypeOf. Um eigene Eigenschaften von geerbten zu unterscheiden, nutzt man hasOwnProperty — beachte, dass for...in die gesamte Kette durchläuft (nur aufzählbare Eigenschaften), während Object.keys ausschließlich eigene Schlüssel zurückgibt.
Auch eingebaute Typen wie arrays, Funktionen und Datumsangaben nutzen diese Kette — ihre Methoden liegen auf Array.prototype, Function.prototype usw. Unter JavaScript Native Prototypes ist erklärt, wie die eingebauten Typen miteinander verbunden sind.
Zusammenfassung
- Jedes object hat ein verstecktes
[[Prototype]], das ein anderes object odernullist. [[Prototype]]liest man mitObject.getPrototypeOf, setzt es mitObject.setPrototypeOf;__proto__ist der veraltete Accessor, und das Ändern eines Prototyps nach der Erstellung ist langsam.- Lesezugriffe durchlaufen die Prototypkette, bis die Eigenschaft gefunden wird oder die Kette bei
nullendet; Schreib- und Löschoperationen wirken immer auf das object selbst und können geerbte Eigenschaften überlagern. thisinnerhalb einer Methode ist das object, auf dem die Methode aufgerufen wurde, nicht der Prototyp, auf dem sie definiert ist.new F()setzt[[Prototype]]des neuen Objekts aufF.prototype; ES6classist syntaktischer Zucker über genau diesen Mechanismus — siehe JavaScript Class Inheritance.