Wie die Prototypenkette in JavaScript funktioniert
Verstehen Sie die Suche in der JavaScript-Prototypenkette, den Unterschied zwischen prototype und [[Prototype]], das Überschreiben geerbter Eigenschaften und die sichere Prüfung von Verknüpfungen.
Beim Lesen einer Property prüft JavaScript zuerst die eigenen Properties des Objekts. Anschließend folgt es der internen [[Prototype]]-Verknüpfung zum nächsten Objekt und wiederholt diesen Vorgang, bis es die Property entweder findet oder null erreicht. In diesem Fall liefert der Lesezugriff undefined.
Die Regel ist einfach. Schwieriger wird es, sobald prototype, __proto__ und [[Prototype]] nebeneinander in einem DevTools-Panel oder einer Interviewfrage auftauchen. Firefox bezeichnet die Verknüpfung beim Aufklappen eines Objekts in der Konsole sogar als <prototype>.
Dieser Artikel behandelt ausschließlich, wie ein Property-Lookup die Kette durchläuft: welches Objekt einen Lesezugriff beantwortet, wo Schreibzugriffe landen und mit welchen APIs Sie einen Prototyp untersuchen. Konstruktorfunktionen, Klassen und Vererbungsmuster mit Object.create werden in A Beginner’s Guide to OOP in JavaScript und Inheritance in JavaScript Object-Oriented Programming behandelt.
Die wichtigsten Erkenntnisse
- Ein Property-Lesezugriff prüft zuerst die eigenen Properties und danach nacheinander jeden
[[Prototype]]. Er liefertundefined, sobald die Kette beinullendet. - Die
prototype-Property einer Funktion wird zum[[Prototype]]der mitnewerzeugten Instanzen. Sie ist nicht der Prototyp der Funktion selbst. __proto__ist ein veralteter Accessor, der vonObject.prototypegeerbt wird. Verwenden Sie stattdessenObject.getPrototypeOf()undObject.setPrototypeOf().- Eine Zuweisung an eine geerbte Property erzeugt in der Regel eine eigene Property, die die geerbte verdeckt (Shadowing). Der Prototyp bleibt dabei unverändert.
Object.create(null)erzeugt ein Objekt ohne Kette und eignet sich daher als sicheres Dictionary für beliebige String-Schlüssel.
Nach welcher Regel erfolgt der Lookup in der Prototypenkette?
Die Prototypenkette in JavaScript ist die über [[Prototype]] verknüpfte Folge von Objekten, die ein Property-Lesezugriff durchläuft, bis er einen Treffer findet oder null erreicht. Jedes gewöhnliche Objekt besitzt einen internen [[Prototype]]-Slot, der auf ein anderes Objekt oder auf null verweist. Der OrdinaryGet-Algorithmus der Spezifikation legt fest, wie ein Lesezugriff funktioniert: Er sucht nach einer eigenen Property und wiederholt den Lookup am Prototyp, falls keine vorhanden ist. Bei den meisten Objekten endet die Kette bei Object.prototype, dessen Prototyp null ist.
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (found on animal)
console.log(rabbit.toString === Object.prototype.toString); // true
console.log(rabbit.flies); // undefined (not found anywhere)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// rabbit -> animal -> Object.prototype -> null
Das Lesen einer fehlenden Property ist kein Fehler. Erst der Aufruf rabbit.flies() löst einen TypeError aus, weil der Aufruf auf undefined erfolgt. Der MDN-Leitfaden Inheritance and the prototype chain beschreibt dasselbe Modell ausführlicher.
Worin unterscheidet sich die prototype-Property einer Funktion vom [[Prototype]] eines Objekts?
Die prototype-Property einer Funktion ist das Objekt, das zum [[Prototype]] der mit new erzeugten Instanzen wird. Sie ist nicht der Prototyp der Funktion selbst – dieser ist Function.prototype. Um die tatsächliche Verknüpfung eines beliebigen Objekts auszulesen, verwenden Sie Object.getPrototypeOf().
class Greeter { hello() { return 'hi'; } }
const g = new Greeter();
console.log(Object.getPrototypeOf(g) === Greeter.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Function.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Greeter.prototype); // false
const arrow = () => {};
console.log(arrow.prototype); // undefined
console.log(Greeter.prototype.hello.prototype); // undefined
Arrow-Funktionen und gewöhnliche Klassenmethoden besitzen überhaupt keine prototype-Property. Ein Lesezugriff auf .prototype liefert bei ihnen daher undefined.
| Name | Was es ist | Wo es liegt | Wie man es ausliest |
|---|---|---|---|
[[Prototype]] | Die interne Verknüpfung, der der Lookup folgt | Jedes Objekt (als interner Slot) | Object.getPrototypeOf(obj) |
obj.__proto__ | Ein Legacy-Getter/-Setter für diese Verknüpfung | Object.prototype | Vermeiden; getPrototypeOf verwenden |
Fn.prototype | Das Objekt, das mit new Fn() erzeugten Instanzen zugewiesen wird | Konstruierbare Funktionen und Klassen | Fn.prototype |
Was ist proto?
__proto__ ist eine Accessor-Property, die auf Object.prototype definiert ist. Sie wird nicht in jedem einzelnen Objekt gespeichert, sodass ein Objekt, das nicht von Object.prototype erbt, sie nicht besitzt. MDN kennzeichnet Object.prototype.__proto__ als „Deprecated“ (veraltet) und erklärt, dass es nur deshalb im Standard enthalten ist, damit ältere Websites weiterhin funktionieren. MDN empfiehlt stattdessen Object.getPrototypeOf() und Object.setPrototypeOf().
const proto = { greet() { return 'hello'; } };
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // true
console.log(Object.hasOwn(obj, '__proto__')); // false
console.log(Object.hasOwn(Object.prototype, '__proto__')); // true
const bare = Object.create(null);
console.log(bare.__proto__); // undefined
const lit = { __proto__: proto };
console.log(Object.getPrototypeOf(lit) === proto); // true
Der Schlüssel { __proto__: value } in einem Objektliteral ist Standardsyntax und vom Accessor unabhängig. Er ist nicht veraltet. Nur die Schreibweise mit Doppelpunkt setzt den Prototyp. Ein berechneter Schlüssel ["__proto__"] erzeugt eine gewöhnliche eigene Property.
Selbst mit der korrekten API sollten Sie den Prototyp eines Objekts nach dessen Erzeugung möglichst nicht mehr ändern. MDN warnt, dies sei „derzeit in jedem Browser und jeder JavaScript-Engine eine sehr langsame Operation“. Legen Sie den Prototyp stattdessen bereits beim Erzeugen des Objekts fest.
Shadowing: Eigene Properties verdecken geerbte
Der Lookup endet beim ersten Treffer. Eine eigene Property verdeckt daher jede gleichnamige Property weiter oben in der Kette. Bei Zuweisungen verhält es sich genauso: Sofern kein geerbter Setter und keine nicht beschreibbare geerbte Property dazwischenkommt, erzeugt oder aktualisiert OrdinarySet laut Spezifikation eine eigene Property auf dem Zielobjekt. Die geerbte Property wird verdeckt, nicht verändert.
const defaults = { theme: 'light' };
const settings = Object.create(defaults);
console.log(settings.theme); // 'light' (inherited)
settings.theme = 'dark'; // creates an own property
console.log(defaults.theme); // 'light' (prototype untouched)
console.log(Object.hasOwn(settings, 'theme')); // true
delete settings.theme;
console.log(settings.theme); // 'light' (inherited again)
// settings -> defaults -> Object.prototype -> null
Object.hasOwn() zeigt Ihnen, welche Ebene einen Lesezugriff beantwortet hat. MDN empfiehlt es anstelle von hasOwnProperty().
Wie gelangen die Methoden von Array.prototype zu jedem Array?
Jedes per Array-Literal erzeugte Array hat Array.prototype als [[Prototype]]. Deshalb funktionieren map, filter und push auf jedem Array, ohne dorthin kopiert zu werden. Auch Built-ins nutzen Shadowing: Array.prototype.toString verdeckt Object.prototype.toString.
const nums = [1, 2, 3];
console.log(Object.getPrototypeOf(nums) === Array.prototype); // true
console.log(Object.hasOwn(nums, 'map')); // false
console.log(nums.map === Array.prototype.map); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(nums.toString()); // '1,2,3'
console.log(Object.prototype.toString.call(nums)); // '[object Array]'
// nums -> Array.prototype -> Object.prototype -> null
Object.create(null) für Objekte ohne Kette
Object.create(null) erzeugt ein Objekt, dessen [[Prototype]] null ist. Es erbt nichts: kein toString, kein hasOwnProperty, keinen __proto__-Accessor. Ein Null-Prototyp-Objekt ist daher ein sicherer Container für Schlüssel, die aus Benutzereingaben stammen. Bei einem einfachen {} liefern geerbte Namen wie constructor bereits einen Wert, bevor Sie überhaupt etwas speichern.
const words = ['a', 'constructor', 'a'];
const plain = {};
for (const w of words) plain[w] = (plain[w] ?? 0) + 1;
console.log(plain.constructor); // a string like "function Object() { [native code] }1"
const counts = Object.create(null);
for (const w of words) counts[w] = (counts[w] ?? 0) + 1;
console.log(counts.constructor); // 1
console.log(counts.a); // 2
console.log('toString' in counts); // false
// counts -> null
Ein Null-Prototyp-Objekt besitzt keine geerbten Methoden. Verwenden Sie daher Object.hasOwn(counts, key) statt counts.hasOwnProperty(key). Die Interpolation in ein Template-Literal löst einen TypeError aus, weil kein toString zur Umwandlung in einen String vorhanden ist. Wenn die Schlüssel keine Strings sind, verwenden Sie eine Map.
Nutzen Klassen dieselbe Prototypenkette?
Ja. Die Klassensyntax führt kein neues Vererbungsmodell ein. Im Klassenrumpf deklarierte Methoden liegen auf ClassName.prototype, und Instanzen erreichen sie über denselben [[Prototype]]-Lookup wie jedes andere Objekt. extends verknüpft das prototype-Objekt einer Klasse mit dem der Elternklasse.
class Animal { speak() { return 'sound'; } }
class Dog extends Animal { bark() { return 'woof'; } }
const d = new Dog();
console.log(typeof Dog); // 'function'
console.log(Object.hasOwn(d, 'speak')); // false
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.speak()); // 'sound'
// d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
Fazit
Jeder Property-Lesezugriff folgt demselben Pfad: JavaScript prüft das Objekt selbst und danach nacheinander jeden [[Prototype]], bis es die Property findet oder null erreicht. Schreibzugriffe verbleiben auf dem Objekt, dem Sie zuweisen. Wenn ein Wert das nächste Mal scheinbar aus dem Nichts kommt, rufen Sie Object.hasOwn() auf dem Objekt auf, wenden dann Object.getPrototypeOf() auf jedes Glied an und notieren die Kette, bis Sie das Objekt finden, das die Property tatsächlich enthält.
FAQs
Worin unterscheiden sich der in-Operator und Object.hasOwn()?
Der in-Operator prüft die gesamte Prototypenkette. 'toString' in {} liefert daher true, weil toString von Object.prototype geerbt wird. Object.hasOwn(obj, key) prüft nur die eigenen Properties des Objekts und liefert für alles Geerbte false. Verwenden Sie in, wenn eine geerbte Property mitzählen soll, und Object.hasOwn(), wenn Sie wissen müssen, ob das Objekt selbst den Schlüssel enthält.
Wie nutzt instanceof die Prototypenkette?
Standardmäßig liefert obj instanceof C den Wert true, wenn C.prototype an irgendeiner Stelle in der Prototypenkette von obj vorkommt. Das beweist nicht, dass C das Objekt erzeugt hat, denn jedes Objekt, dessen Kette C.prototype enthält, besteht die Prüfung. Eine Klasse kann diese Prüfung ersetzen, indem sie eine statische Methode Symbol.hasInstance definiert. Primitive Werte auf der linken Seite liefern bei der Standardprüfung immer false.
Berücksichtigen for...in-Schleifen und Object.keys() geerbte Properties?
Eine for...in-Schleife durchläuft aufzählbare Properties mit String-Schlüssel auf dem Objekt selbst und auf jedem Objekt weiter oben in seiner Prototypenkette. Object.keys(), Object.entries() und Object Spread berücksichtigen nur die eigenen aufzählbaren Properties des Objekts. Methoden auf eingebauten Prototypen wie Object.prototype und Array.prototype sind nicht aufzählbar und erscheinen daher nicht in for...in. Aufzählbare Properties auf Ihren eigenen Prototyp-Objekten dagegen schon.
Kann eine Prototypenkette in JavaScript einen Zyklus enthalten?
Bei gewöhnlichen Objekten nicht. Object.setPrototypeOf() löst einen TypeError aus, wenn der neue Prototyp einen Zyklus erzeugen würde – etwa wenn der Prototyp von a auf b gesetzt wird, obwohl b bereits von a erbt. Eine Zuweisung über den __proto__-Setter löst denselben Fehler aus. Da Zyklen abgewiesen werden, ist eine Kette gewöhnlicher Objekte stets endlich und endet bei null. Ein Property-Lookup terminiert daher immer.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k