Prototypen und Vererbung in JavaScript: Die Prototype-Kette verstehen

Warum funktioniert .toString() auf jedem Objekt, obwohl du es nie definiert hast? Die Antwort ist die Prototype-Kette – das Herzstück von Vererbung in JavaScript.

Teilen

Hast du dich schon einmal gefragt, warum du auf jedem beliebigen Array .map() oder .filter() aufrufen kannst, obwohl du diese Methoden nie selbst definiert hast? Oder warum "hallo".toUpperCase() einfach funktioniert? Die Antwort liegt in einem der grundlegendsten – und am häufigsten missverstandenen – Konzepte der Sprache: der Prototype-Kette. In diesem Beitrag schauen wir uns Schritt für Schritt an, wie Vererbung in JavaScript wirklich funktioniert.

Jedes Objekt hat einen Prototyp

In JavaScript besitzt fast jedes Objekt eine versteckte Referenz auf ein anderes Objekt: seinen Prototyp. Wenn du auf eine Eigenschaft zugreifst, die das Objekt selbst nicht hat, sucht JavaScript automatisch in dessen Prototyp weiter – und in dessen Prototyp – bis die Eigenschaft gefunden wird oder die Kette bei null endet.

Diesen internen Prototyp kannst du dir mit Object.getPrototypeOf() ansehen:

const person = { name: "Ada" };

// person hat selbst keine toString-Methode ...
console.log(person.hasOwnProperty("toString")); // false

// ... aber der Aufruf funktioniert trotzdem:
console.log(person.toString()); // "[object Object]"

// Weil der Prototyp Object.prototype diese Methode bereitstellt:
console.log(Object.getPrototypeOf(person) === Object.prototype); // true

Das Objekt person selbst kennt nur name. Der Aufruf von toString() wandert eine Stufe höher zu Object.prototype, wo die Methode tatsächlich definiert ist.

Wie die Kette durchlaufen wird

Die Suche nach einer Eigenschaft folgt immer derselben Regel: zuerst das Objekt selbst, dann sein Prototyp, dann dessen Prototyp – und so weiter. Man spricht deshalb von der Prototype-Kette (englisch prototype chain).

const basis = { gruss: "Hallo" };

// erben-Objekt bekommt basis als Prototyp
const erben = Object.create(basis);
erben.name = "Grace";

console.log(erben.name);  // "Grace"  -> eigene Eigenschaft
console.log(erben.gruss); // "Hallo"  -> geerbt vom Prototyp

// Am Ende der Kette steht immer null:
console.log(Object.getPrototypeOf(basis) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype));           // null

Mit Object.create() erstellst du gezielt ein neues Objekt und legst dessen Prototyp fest. Das ist die reinste Form der prototypischen Vererbung in JavaScript.

Konstruktorfunktionen und die prototype-Eigenschaft

Vor der class-Syntax wurden Objekte meist über Konstruktorfunktionen erzeugt. Jede Funktion in JavaScript besitzt automatisch eine Eigenschaft namens prototype. Erstellst du mit new ein Objekt, wird genau dieses prototype-Objekt zum Prototyp der neuen Instanz.

function Tier(name) {
  this.name = name;
}

// Methode einmal am Prototyp definieren - nicht pro Instanz!
Tier.prototype.sprich = function () {
  return this.name + " macht ein Geräusch.";
};

const katze = new Tier("Minka");
console.log(katze.sprich()); // "Minka macht ein Geräusch."

// katze selbst hat sprich nicht, aber Tier.prototype schon:
console.log(katze.hasOwnProperty("sprich")); // false
console.log(Object.getPrototypeOf(katze) === Tier.prototype); // true

Der entscheidende Vorteil: Die Methode sprich existiert nur ein einziges Mal im Speicher – am Prototyp. Alle Instanzen teilen sie sich, statt jeweils eine eigene Kopie zu halten.

class ist nur Zuckerguss

Seit ES2015 gibt es die class-Syntax. Sie sieht aus wie klassische Vererbung aus Java oder C++, aber unter der Haube passiert exakt dasselbe wie oben: Methoden landen am Prototyp, und extends verkettet zwei Prototypen miteinander.

class Tier {
  constructor(name) {
    this.name = name;
  }
  sprich() {
    return this.name + " macht ein Geräusch.";
  }
}

class Hund extends Tier {
  sprich() {
    return this.name + " bellt.";
  }
}

const bello = new Hund("Bello");
console.log(bello.sprich()); // "Bello bellt."

// Die Kette: bello -> Hund.prototype -> Tier.prototype -> Object.prototype
console.log(bello instanceof Hund);  // true
console.log(bello instanceof Tier);  // true

Ruft JavaScript bello.sprich() auf, findet es die Methode direkt in Hund.prototype und hält dort an. Hätte Hund keine eigene sprich-Methode, würde die Suche automatisch bei Tier.prototype weitermachen. Genau so entsteht das Überschreiben von Methoden.

Häufige Stolperfallen

Ein paar Dinge solltest du im Hinterkopf behalten, damit dich die Prototype-Kette nicht überrascht:

  • Eigene vs. geerbte Eigenschaften: Mit obj.hasOwnProperty("x") prüfst du, ob eine Eigenschaft direkt am Objekt liegt oder nur geerbt ist. Das ist wichtig, wenn du mit for...in über Objekte iterierst.
  • Prototypen nicht zur Laufzeit verbiegen: Zwar kannst du mit Object.setPrototypeOf() den Prototyp nachträglich ändern, das ist aber langsam und macht Code schwer nachvollziehbar. Lege die Struktur lieber gleich fest.
  • Built-ins nicht erweitern: Ergänze niemals Array.prototype oder Object.prototype um eigene Methoden. Andere Bibliotheken könnten kollidieren, und for...in-Schleifen sehen plötzlich deine Zusätze.
const arr = [1, 2, 3];

// Warum funktioniert map hier? Wegen der Kette:
// arr -> Array.prototype (map, filter, ...) -> Object.prototype -> null
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(arr.map((n) => n * 2)); // [2, 4, 6]

Fazit

Die Prototype-Kette ist kein exotisches Detail, sondern das Fundament, auf dem Vererbung in JavaScript aufbaut. Wenn du verstehst, dass jede Eigenschaftssuche das Objekt selbst und dann Stufe für Stufe seine Prototypen durchläuft, wird plötzlich vieles klar: warum .map() auf Arrays existiert, wie class und extends intern arbeiten und warum geteilte Methoden am Prototyp Speicher sparen. Nutze in der Praxis meist die class-Syntax für Lesbarkeit – aber jetzt weißt du, was darunter wirklich passiert. Spiel ruhig in der Konsole mit Object.getPrototypeOf() herum, um die Ketten deiner eigenen Objekte sichtbar zu machen.