Jedes Objekt hat einen Prototype
JavaScript ist eine prototypbasierte Sprache. Klingt erstmal exotisch, ist aber eigentlich ganz simpel: Jedes Objekt hat eine versteckte Verknüpfung zu einem anderen Objekt — seinem Prototype — und wenn du auf eine Eigenschaft zugreifst, die das Objekt selbst gar nicht besitzt, folgt JavaScript dieser Verknüpfung und schaut dort nach.
rabbit hat selbst keine Eigenschaft eats. JavaScript schaut also zuerst in rabbit, wird dort nicht fündig, hangelt sich über den Prototype-Link zu animal weiter, findet eats: true und gibt den Wert zurück. Bei flies läuft es die komplette Kette ab, findet nichts und liefert undefined.
Genau dieses Nachschlagen entlang der Kette ist der ganze Mechanismus. Vererbung, Methoden, class – all das basiert darauf.
Die Prototype Chain in JavaScript
Die Kette endet nicht nach einem Schritt. Jeder Prototype kann selbst wieder einen Prototype haben, und so weiter – bis man schließlich bei null landet:
Führst du das aus, bekommst du erst rabbit, dann Object.prototype, dann null. Genau deshalb funktioniert rabbit.toString(), obwohl du toString nirgends definiert hast — die Methode sitzt auf Object.prototype, dem obersten Glied fast jeder Prototype Chain.
Beim Lesen einer Eigenschaft wandert JavaScript diese Kette von unten nach oben durch. Beim Schreiben dagegen landet der Wert immer direkt auf dem Objekt selbst — nach oben geschaut wird dabei nie. Diese Asymmetrie ist wichtig und sorgt regelmäßig für Stirnrunzeln.
Konstruktorfunktionen und .prototype
Bevor es class gab, war der übliche Weg, viele ähnliche Objekte zu erzeugen, eine Konstruktorfunktion, die man mit new aufruft:
Zwei Dinge passieren, wenn du new User("Ada") aufrufst:
- Ein frisches Objekt wird erzeugt, dessen Prototyp auf
User.prototypegesetzt wird. Userläuft, wobeithisan dieses neue Objekt gebunden ist.
greet wird dabei nicht in jede Instanz kopiert. Die Methode existiert genau einmal auf User.prototype, und sowohl ada als auch boris finden sie, indem sie ihre Prototype Chain hochlaufen. Deshalb gibt die letzte Zeile true aus – es ist buchstäblich dieselbe Funktion.
prototype vs. __proto__
An diesen beiden Namen stolpert wirklich jeder. Sie hängen zwar zusammen, sind aber nicht dasselbe.
User.prototypeist eine Eigenschaft der Konstruktorfunktion. Es ist das Objekt, das zum Prototyp aller Instanzen wird, die man mitnew User(...)erzeugt.ada.__proto__(bzw.Object.getPrototypeOf(ada)) ist die Verbindung auf der Instanz selbst, die nach oben auf ihren Prototyp zeigt.
In neuem Code solltest du lieber Object.getPrototypeOf(obj) statt obj.__proto__ verwenden. __proto__ ist nur noch ein Legacy-Accessor, der aus Kompatibilitätsgründen existiert – die Funktion ist die offizielle API.
Klassen sind nur Syntaxzucker
Seit ES6 kannst du in JavaScript zwar mit class arbeiten, unter der Haube laufen aber weiterhin Prototypes. Schauen wir uns beide Varianten direkt nebeneinander an:
greet liegt auf User.prototype — genau dort, wo sie auch gelandet wäre, wenn du sie von Hand gesetzt hättest. Das class-Keyword bringt dir vor allem sauberere Syntax, strengere Regeln (ohne new geht nichts) und eine elegantere Variante für extends. Am Laufzeitmodell ändert sich dadurch aber nichts.
Das zu wissen hilft dir beim Lesen von Fehlermeldungen und beim Debuggen von this. Wenn im Stack "User.prototype.greet" auftaucht, ist das kein kryptischer interner Name — die Methode wohnt tatsächlich genau da.
Vererbung ist einfach eine längere Prototype Chain
extends hängt einen Prototype an den nächsten. Der Prototype der Elternklasse wird zum Prototype des Prototypes der Kindklasse:
Beim Zugriff auf rex.eat hangelt sich JavaScript die Prototypenkette entlang: rex → Dog.prototype → Animal.prototype. Dort findet es eat und ruft die Methode auf — wobei this weiterhin an rex gebunden bleibt. Mehr macht extends im Grunde nicht: Es baut diese Kette für dich auf.
Objekte direkt mit einem Prototyp erzeugen
Einen Konstruktor brauchst du dafür gar nicht. Mit Object.create(proto) erzeugst du ein neues Objekt, das genau den Prototyp bekommt, den du angibst:
Kein class, kein new, keine Konstruktorfunktion. Zwei Objekte, die sich eine Methode über einen gemeinsamen Prototyp teilen. Das ist die nackte Grundform der prototypischen Vererbung in JavaScript – alles andere baut darauf auf.
hasOwnProperty: Eigene vs. geerbte Eigenschaften
Da die Eigenschaftssuche die komplette Prototype Chain durchläuft, liefert "foo" in obj auch für geerbte Eigenschaften true. Willst du also wissen, ob eine Eigenschaft wirklich dem Objekt selbst gehört, nimmst du Object.hasOwn (oder das ältere hasOwnProperty):
name liegt auf der Instanz, greet dagegen auf dem Prototype. Der in-Operator findet beides, Object.hasOwn nur Ersteres. Das wird relevant, sobald du mit for...in über ein Objekt iterierst oder es serialisierst – in der Regel willst du ja nur die eigenen Properties erwischen.
Finger weg vom Monkey-Patching eingebauter Prototypes
Da sich alle Arrays in deinem Programm denselben Array.prototype teilen, könntest du dort eigene Methoden ergänzen:
// Bitte nicht.
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
Das Problem ist nicht, dass es nicht funktioniert — es funktioniert ja. Das Problem ist, dass jede Library, jede Abhängigkeit und jede zukünftige JavaScript-Version sich diesen Namespace jetzt mit dir teilt. Sobald Array.prototype.last irgendwann als echte Methode mit leicht abweichender Semantik ausgeliefert wird, bricht dein Code (oder der von jemand anderem) auf subtile Weise. Die Geschichte rund um Array.prototype.flatten bzw. Array.prototype.flat ist das klassische Lehrstück dazu.
Halte Helper lieber als eigenständige Funktionen:
Eine geteilte Oberfläche weniger, an der man sich stoßen kann.
Das mentale Modell
Wenn man alles Drumherum weglässt, laufen Prototypen in JavaScript auf drei Regeln hinaus:
- Jedes Objekt hat eine Prototype-Verknüpfung (notfalls
null). - Beim Lesen von Properties wandert JS die Kette nach oben – beim Schreiben nicht.
class,newundextendssind nur bequeme Wege, diese Ketten aufzubauen, ohne selbstObject.createtippen zu müssen.
Hat man diese drei Punkte im Kopf, ergeben sich das Verhalten von this, instanceof, die Methodenauflösung und die Vererbung ganz von allein.
Weiter geht's: die Event Loop
Mit den Prototypen ist das Objektmodell abgehakt. Im nächsten Kapitel wird's ganz anders – da geht es darum, wie JavaScript deinen Code über die Zeit tatsächlich ausführt. Die Event Loop ist der Grund, warum sich Timer, Promises und async/await so verhalten, wie sie es tun – und sie ist das Fundament für alles Asynchrone.
Häufig gestellte Fragen
Was ist ein Prototype in JavaScript?
Jedes Objekt in JavaScript hat eine interne Referenz auf ein anderes Objekt — seinen Prototype. Greifst du auf eine Property zu, die am Objekt selbst nicht existiert, hangelt sich JavaScript diese Kette entlang nach oben (die sogenannte Prototype Chain) und sucht dort weiter. Genau deshalb musst du eine Methode nur einmal definieren, und trotzdem haben alle Instanzen Zugriff darauf.
Wo liegt der Unterschied zwischen __proto__ und prototype?
prototype ist eine Property an Konstruktor-Funktionen (und Klassen). Dieses Objekt wird zum Prototype der Instanzen, die du per new erzeugst. __proto__ — beziehungsweise sauberer Object.getPrototypeOf(obj) — ist dagegen die tatsächliche Verknüpfung an einer Instanz, die auf ihren Prototype zeigt. Kurz: instance.__proto__ === Constructor.prototype.
Sind JavaScript-Klassen nur Syntactic Sugar für Prototypes?
Im Großen und Ganzen ja. class Foo { bar() {} } legt bar genau so auf Foo.prototype ab, als hättest du function Foo(){} und Foo.prototype.bar = function(){} geschrieben. Klassen bringen zwar Private Fields, strengere Semantik und eine angenehmere Syntax für extends und super mit — aber darunter werkelt nach wie vor der Prototype-Mechanismus.
Sollte ich Methoden an Built-in-Prototypes wie Array.prototype hängen?
So gut wie nie. Wenn du Array.prototype oder Object.prototype veränderst, betrifft das jedes Array bzw. jedes Objekt in deinem Programm — inklusive der Objekte aus fremden Libraries. Das kann mit künftigen Spracherweiterungen kollidieren und dir for...in-Schleifen zerschießen. Pack deine Helper lieber in eigene Funktionen oder Module.