# Prototypen: das Objektmodell hinter JavaScript-Klassen

URL: https://www.mikebild.dev/de/blog/javascript-prototypen-modell-hinter-class/

```js
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name + ' makes a sound';
  }
}

const minka = new Animal('Minka');
const rex = new Animal('Rex');

minka.speak === rex.speak;                         // true
minka.hasOwnProperty('speak');                     // false
Object.getPrototypeOf(minka) === Animal.prototype; // true
```

Drei Zeilen Ausgabe, und in ihnen steckt der ganze Artikel. Die Methode `speak` existiert genau einmal, obwohl zwei Instanzen sie aufrufen. Keiner der beiden Instanzen gehört sie – `hasOwnProperty` sagt nein. Gefunden wird sie an einem dritten Objekt, `Animal.prototype`, auf das beide Instanzen mit einem internen Verweis zeigen.

Dieses dritte Objekt hat `class` nicht erfunden, es hat es nur aus dem Blickfeld geräumt. Der Verweis darauf trägt in JavaScript jede Methodensuche, jede Vererbung und jeden verlorenen `this`-Bezug. Deshalb baue ich das Modell hier von unten auf: erst reine Prototypen mit `Object.create`, dann Konstruktor-Funktionen und der `new`-Ablauf, dann `class` als die Fassade, die beides zusammenfasst.

## Jedes Objekt hat einen Link nach oben

Das Fundament ist erstaunlich schlicht. Jedes Objekt in JavaScript trägt einen internen Verweis, in der Spezifikation `[[Prototype]]` genannt. Dieser Verweis zeigt entweder auf ein anderes Objekt – den Prototyp – oder auf `null`. Wenn du auf eine Property zugreifst, die das Objekt selbst nicht besitzt, schaut die Sprache nicht auf und gibt `undefined` zurück. Sie folgt zunächst dem `[[Prototype]]`-Link und sucht dort weiter. Und findet sie dort nichts, geht sie einen Link höher. So entsteht die Prototyp-Kette, und sie endet bei `null`.

Am klarsten sieht man das ohne jede `class` und ohne `new`, nur mit `Object.create`:

```js
const animal = {
  describe() {
    return this.name + ' makes ' + this.sound;
  }
};

const dog = Object.create(animal);
dog.name = 'Rex';
dog.sound = 'woof';

dog.describe(); // 'Rex makes woof'

Object.getPrototypeOf(dog) === animal; // true
dog.hasOwnProperty('describe');        // false
dog.hasOwnProperty('name');            // true
```

Schau dir an, was hier geschieht. Das Objekt `dog` besitzt selbst nur `name` und `sound` – das bestätigt `hasOwnProperty`. Die Methode `describe` findet es nicht bei sich, sondern erst einen Schritt höher, bei `animal`. Beim Aufruf zählt aber, wer der Empfänger war: `this` ist `dog`, deshalb liest `describe` `Rex` und `woof`. Die Methode liegt am Prototyp, die Daten liegen an der Instanz, und der Lookup verbindet beides. Das ist der ganze Mechanismus – keine Klassen, keine Konstruktoren, nur ein Objekt, das auf ein anderes zeigt.

`Object.create` macht diesen Link explizit sichtbar: Der erste Parameter wird direkt zum `[[Prototype]]` des neuen Objekts. Du kannst sogar `Object.create(null)` schreiben und bekommst ein Objekt ganz ohne Prototyp – ohne `toString`, ohne `hasOwnProperty`, ein blankes Wörterbuch. Nützlich, wenn man ein Objekt als reine Key-Value-Ablage will und keine geerbten Eigenschaften im Weg haben möchte.

## Die Kette, visualisiert

Wenn `dog.describe()` aufgerufen wird, läuft der Lookup die Kette hoch. Das folgende Diagramm zeigt die Stationen bis zum Ende bei `null`:

```mermaid
graph TD
  A["dog<br/>own: name, sound"] -->|"[[Prototype]]"| B["animal<br/>own: describe"]
  B -->|"[[Prototype]]"| C["Object.prototype<br/>own: hasOwnProperty, toString"]
  C -->|"[[Prototype]]"| D["null"]
```

Jeder Pfeil ist ein `[[Prototype]]`-Verweis, und der Lookup steigt an ihnen entlang auf. Sucht die Sprache `describe`, wird sie bei `animal` fündig. Suchte sie `toString`, ginge es eine Station weiter bis `Object.prototype`. Und suchte sie etwas, das es nirgends gibt, käme sie bei `null` an und lieferte `undefined`. Diese Kette ist kein Sonderfall für selbstgebaute Objekte – jedes Objekt, jedes Array, jede Funktion hängt an so einer Kette, deren Wurzel `Object.prototype` ist.

## Konstruktor-Funktionen: der Weg vor `class`

Vor ES2015 baute man wiederverwendbare „Typen“ mit Konstruktor-Funktionen. Der Trick sitzt in zwei Teilen, die man auseinanderhalten muss. Erstens: Jede Funktion besitzt eine Property namens `prototype` – ein gewöhnliches Objekt, das als Vorlage dient. Zweitens: Der `new`-Operator verbindet ein frisches Objekt mit genau dieser Vorlage.

```js
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  return this.name + ' makes a sound';
};

const cat = new Animal('Minka');
cat.speak(); // 'Minka makes a sound'

Object.getPrototypeOf(cat) === Animal.prototype; // true
cat.hasOwnProperty('speak');                     // false
```

Was tut `new Animal('Minka')` konkret? Der Ablauf ist – vereinfacht – dreistufig. Zuerst wird ein neues, leeres Objekt erzeugt, dessen `[[Prototype]]` auf `Animal.prototype` gesetzt wird. Dann wird `Animal` aufgerufen, wobei `this` auf dieses neue Objekt zeigt – so landet `this.name = name` als eigene Property an der Instanz. Und schließlich: Gibt die Funktion selbst kein Objekt zurück, wird das neue Objekt das Ergebnis. Deshalb steht `speak` nicht an der Instanz, sondern an `Animal.prototype`, und wird von allen Katzen geteilt.

Vererbung war vor `class` Handarbeit – und genau diese Handarbeit zeigt, was `extends` später automatisiert:

```js
function Dog(name) {
  Animal.call(this, name);       // Parent-Konstruktor auf eigenem this
}

Dog.prototype = Object.create(Animal.prototype); // Kette einhängen
Dog.prototype.constructor = Dog;                 // constructor wieder setzen

Dog.prototype.speak = function () {
  return this.name + ' makes woof';
};

const rex = new Dog('Rex');
rex.speak();                 // 'Rex makes woof'
rex instanceof Dog;          // true
rex instanceof Animal;       // true
```

Drei Zeilen tragen hier die Last. `Animal.call(this, name)` ruft den Eltern-Konstruktor auf dem eigenen `this` auf, damit `name` gesetzt wird. `Dog.prototype = Object.create(Animal.prototype)` hängt die Prototyp-Kette ein, sodass eine Dog-Instanz erst bei `Dog.prototype`, dann bei `Animal.prototype`, dann bei `Object.prototype` sucht. Und die dritte Zeile repariert etwas, das die zweite kaputtgemacht hat: Beim Überschreiben von `Dog.prototype` ging die `constructor`-Rückverweisung verloren, die wir manuell wiederherstellen. Genau solche Details sind es, die man in der Praxis vergisst – und die `class` einem abnimmt.

## Dieselbe Sache als `class`

Jetzt derselbe Code in ES2015-Syntax, Seite an Seite gedacht. Das Ergebnis ist identisch im Objektmodell – dieselben Prototyp-Objekte, dieselbe Kette, dieselben geteilten Methoden:

```js
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name + ' makes a sound';
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);
  }
  speak() {
    return this.name + ' makes woof';
  }
}

const rex = new Dog('Rex');
Object.getPrototypeOf(rex) === Dog.prototype;              // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
```

Vergleiche das mit der ES5-Fassung, und die Übersetzung wird direkt sichtbar. Die Methoden `speak` im Class-Body landen auf `Animal.prototype` bzw. `Dog.prototype` – nicht an der Instanz. `extends Animal` erledigt das Einhängen der Kette, für das wir eben `Object.create(Animal.prototype)` von Hand geschrieben haben. Und `super(name)` ersetzt das manuelle `Animal.call(this, name)`. Die letzte Zeile beweist es: `rex` zeigt auf `Dog.prototype`, und dieses zeigt auf `Animal.prototype`. Dieselbe Kette wie vorher, nur ohne den Boilerplate und ohne die vergessene `constructor`-Zeile.

Ehrlich bleiben muss man trotzdem: `class` ist Syntax-Zucker, aber nicht ausschließlich. Ein Class-Body läuft immer im Strict Mode. Eine Klasse lässt sich nicht ohne `new` aufrufen – der Versuch wirft einen Fehler, während eine Konstruktor-Funktion sich auch als normale Funktion missbrauchen ließe. Und `class`-Deklarationen werden nicht wie `function`-Deklarationen an den Anfang gezogen und initialisiert; sie liegen in einer temporalen toten Zone und sind vor ihrer Definitionszeile nicht benutzbar. Auch `super` in Methoden greift auf einen internen Mechanismus (`[[HomeObject]]`) zurück, den man mit reinem ES5 nicht 1:1 nachbaut. Die Fassade ist also bequem und an ein paar Kanten strenger als das Original – aber das Modell darunter bleibt Prototyp.

## `prototype`, `__proto__` und `Object.create` sauber trennen

Die häufigste Verwirrung, die mir in Code-Reviews begegnet, dreht sich um zwei ähnlich klingende Namen. `prototype` und `__proto__` sind nicht dasselbe, und die Verwechslung produziert Fehler, die man lange anstarrt. Diese Liste sortiert die drei Werkzeuge:

- Eine konstruierbare Funktion besitzt üblicherweise eine eigene `prototype`-Property. `new` verwendet das dort referenzierte Objekt als Prototyp der Instanz. Gewöhnliche Instanzen haben keine eigene `prototype`-Property; Pfeilfunktionen und Methoden sind nicht konstruierbar und besitzen ebenfalls keine solche Vorlage.
- `__proto__` (bzw. das interne `[[Prototype]]`) ist der tatsächliche Kettenlink, den jedes Objekt trägt. Der kanonische Zugriff läuft über `Object.getPrototypeOf(obj)`; `__proto__` selbst ist in ES2015 nur über Annex B standardisiert und gilt als Legacy.
- `Object.create(proto)` erzeugt ein Objekt und setzt dessen `[[Prototype]]` direkt auf `proto` – ganz ohne Konstruktor-Funktion. Das ist der direkte Weg, einen Kettenlink zu legen.

Der entscheidende Unterschied: `Fn.prototype` ist die Vorlage auf der Funktion, `instance.[[Prototype]]` ist der Link auf dem erzeugten Objekt. Beim Aufruf von `new Fn()` wird das eine zum anderen. Und um es endgültig zu entwirren – die Funktion selbst ist auch ein Objekt und hat ihren eigenen Link:

```js
function Fn() {}
const obj = new Fn();

Object.getPrototypeOf(obj) === Fn.prototype; // true  (Instanz-Link)
obj.prototype;                               // undefined (Instanz hat keine)
Object.getPrototypeOf(Fn) === Function.prototype; // true (Fn ist selbst Objekt)
```

Achte auf die letzte Zeile. `Fn.__proto__` ist `Function.prototype`, nicht `Fn.prototype`. Wer diese beiden verwechselt, sucht Methoden an der falschen Stelle. Im Zweifel hilft immer `Object.getPrototypeOf`, um die Kette Schritt für Schritt abzugehen, statt zu raten.

## Der teuerste Fallstrick: Methoden im Konstruktor

Jetzt zu dem Punkt, der in echten Codebasen Speicher kostet und in Reviews oft durchrutscht. Es ist ein handfester Unterschied, ob eine Methode am Prototyp liegt oder im Konstruktor erzeugt wird:

```js
function WidgetBad() {
  this.render = function () { /* ... */ }; // pro Instanz eine neue Funktion
}

function WidgetGood() {}
WidgetGood.prototype.render = function () { /* ... */ }; // geteilt

const a1 = new WidgetBad();
const a2 = new WidgetBad();
a1.render === a2.render; // false  -> zwei getrennte Funktionen

const b1 = new WidgetGood();
const b2 = new WidgetGood();
b1.render === b2.render; // true   -> eine Funktion, geteilt
```

Der Beweis steht in den Vergleichen. Bei `WidgetBad` bekommt jede Instanz ihre eigene `render`-Funktion – bei tausend Widgets sind das tausend identische Funktionen im Speicher. Bei `WidgetGood` liegt `render` einmal an `WidgetGood.prototype`, und alle Instanzen teilen sie sich. Genau das tut `class` automatisch: Methoden im Class-Body landen am Prototyp, nicht an der Instanz. Man muss sich für den guten Fall in `class` also gar nicht anstrengen – man muss ihn nur nicht durch eine Zuweisung im `constructor` zerstören.

Der zweite häufige Fallstrick ist der Verlust von `this`. Eine Prototyp-Methode kennt ihren Empfänger nur, wenn sie als `obj.method()` aufgerufen wird. Reißt man sie los, ist der Bezug weg:

```js
const cat = new Animal('Minka');

const fn = cat.speak;
fn(); // this ist nicht mehr cat -> Fehler oder falsches Ergebnis

const bound = cat.speak.bind(cat);
bound(); // 'Minka makes a sound' -> this wieder gebunden
```

Das ist kein Bug, sondern eine direkte Folge des Modells: `this` wird beim Aufruf bestimmt, nicht bei der Definition. Löst man `cat.speak` in eine freie Variable, fehlt der Empfänger. Die Werkzeuge dagegen sind 2017 etabliert: `.bind(cat)` erzeugt eine fest gebundene Variante, oder man wickelt den Aufruf in eine Arrow-Function, die `this` aus dem umgebenden Bereich übernimmt. Ein Ausblick am Rande: An Klassen-Feldern mit Arrow-Funktionen wird gearbeitet, die genau dieses Binden eleganter machen sollen – das ist Ende 2017 aber noch ein TC39-Proposal und kein Standard. Wer es heute produktiv braucht, greift zu `bind` oder Arrow-Wrapper.

## Warum sich das auszahlt

Man kann JavaScript jahrelang mit `class` schreiben, ohne je „Prototyp“ zu sagen. Bis zu dem Tag, an dem `this` in einem Event-Handler `undefined` ist, an dem eine geerbte Methode nicht gefunden wird, oder an dem ein Profiler tausendfach dieselbe Funktion im Heap zeigt. Dann trennt sich, wer das Modell kennt, von dem, der rät. Die drei wiederkehrenden Symptome – verlorenes `this`, Methode an der falschen Stelle, Speicher pro Instanz – sind allesamt keine Marotten der Sprache, sondern logische Konsequenzen einer Kette aus Objekten, die aufeinander zeigen.

`class` hat seine Berechtigung: lesbar, strikt, und es nimmt einem den fehleranfälligen Boilerplate der ES5-Vererbung ab. Aber es ist eine Fassade, und hinter der Fassade steht immer noch `Object.create`, `Fn.prototype` und ein `new`, das beides verbindet. Wenn du das nächste Mal `extends` schreibst, denk kurz an die drei Zeilen, die es ersetzt – die eingehängte Kette, den `super`-Aufruf, den `constructor`-Rückverweis. Dann ist `class` keine Magie mehr, sondern eine Abkürzung, deren Langform du jederzeit selbst hinschreiben könntest.

Dieses prototypische Denken ist derselbe rote Faden, der sich durch die [JavaScript-Patterns zieht, die geblieben sind](https://www.mikebild.dev/de/blog/javascript-patterns-module-hof-currying/) – Objekte, die auf Objekte zeigen, Funktionen als Bausteine. Wer dieses Fundament versteht, baut auf Frameworks ruhiger auf.

## Weiterführende Quellen

- MDN, Inheritance and the prototype chain: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
- MDN, Object.create: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
- MDN, Object.getPrototypeOf: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf
- MDN, Object.setPrototypeOf: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
- MDN, Object.prototype.\_\_proto\_\_: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
- MDN, Classes: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
- MDN, new operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
- ECMAScript 2015 (ES6) Spec: https://www.ecma-international.org/ecma-262/6.0/
- ECMAScript 2017 (ES8) Spec: https://www.ecma-international.org/ecma-262/8.0/
