Wenn man in JavaScript Objekte über eine sogenannte Konstruktorfunktion (eine Art Bauplan oder Fabrik für Objekte) erstellt, möchte man diesen Objekten oft gemeinsame Fähigkeiten mitgeben – zum Beispiel Methoden (Funktionen).
Genau hier kommt die Eigenschaft prototype ins Spiel: Mit ihr hängen wir
Eigenschaften oder Methoden direkt an die Konstruktorfunktion an. Das Geniale
daran ist: Diese Methode wird dadurch nicht in jedem einzelnen erzeugten Objekt neu kopiert,
sondern existiert nur ein einziges Mal an zentraler Stelle. Alle erstellten Objekte "erben"
bzw. teilen sich diese Funktion.
prototype ist das Werkzeug in JavaScript, um
Vererbung zu realisieren und Code im Speicher hocheffizient zu teilen.
Um das System dahinter wirklich zu verstehen, vergessen wir für einen kurzen Moment die klassische objektorientierte Programmierung (wie in Java oder C++), bei der Klassen eine art starre „Gussform“ sind. In JavaScript gibt es im Hintergrund eigentlich keine echten Klassen – alles basiert auf Objekten, die von anderen Objekten erben.
Hier ist die Erklärung, aufgeteilt in die zwei Dinge, die am häufigsten verwechselt werden: Das Prototyp-Objekt und die Prototypen-Kette.
Das größte Verwirrspiel in JavaScript entsteht, weil das Wort „Prototype“ an zwei verschiedenen Stellen etwas völlig Unterschiedliches bedeutet:
__proto__ (oder intern [[Prototype]]): Jedes Objekt in JavaScript hat diese geheime Verbindung. Es ist ein interner Zeiger, der auf das Objekt verweist, von dem es erbt. Es ist quasi der „Eltern-Link“.
Die Eigenschaft prototype: Diese Eigenschaft haben nur Konstruktor-Funktionen (bzw. Funktionen im Allgemeinen). Sie ist nicht der Prototyp der Funktion selbst, sondern das Objekt, das als „Blaupause“ an alle Objekte übergeben wird, die mit new aus dieser Funktion erstellt werden.
Nehmen wir an, wir bauen ein einfaches Spiel oder eine Anwendung und brauchen mehrere "Spieler"-Objekte.
// 1. Die Konstruktor-Funktion (Die Fabrik)
function Spieler(name, punkte) {
this.name = name;
this.punkte = punkte;
}
// 2. Wir hängen eine Methode an das 'prototype'-Objekt der Funktion
Spieler.prototype.feiereSieg = function() {
console.log(`${this.name} hat gewonnen und hat ${this.punkte} Punkte!`);
};
// 3. Wir erstellen neue Instanzen
const spieler1 = new Spieler("Anna", 100);
const spieler2 = new Spieler("Ben", 85);
Was passiert hier im Speicher?
spieler1 und spieler2 haben jeweils ihre eigenen Eigenschaften name und punkte.
Die Methode feiereSieg existiert aber nur ein einziges Mal – und zwar auf Spieler.prototype.
Sowohl spieler1 als auch spieler2 haben eine unsichtbare Verknüpfung (__proto__), die direkt auf Spieler.prototype zeigt.
Wenn du jetzt aufrufst:
spieler1.feiereSieg();
...schaut JavaScript zuerst in spieler1 nach: "Hast du eine Methode namens feiereSieg?" Die Antwort ist Nein. Anstatt aufzugeben, folgt JavaScript der Prototypen-Kette über den internen Link zu Spieler.prototype und fragt dort: "Hast du feiereSieg?" Dort wird sie gefunden und im Kontext von spieler1 (wegen this) ausgeführt.
Würdest du die Methode direkt in der Konstruktor-Funktion definieren...
function SchlechterSpieler(name) {
this.name = name;
this.feiereSieg = function() { ... }; // Schlecht!
}
...dann würde bei 1.000 Spielern auch 1.000 Mal die exakt gleiche Funktion im Arbeitsspeicher angelegt. Nutzt du hingegen prototype, existiert die Funktion nur einmal im Speicher, egal ob du 2 oder 10.000 Spieler erstellst. Das spart massiv Ressourcen.
Seit ES6 nutzen wir meistens das Schlüsselwort class. Das sieht dann so aus:
class Spieler {
constructor(name, punkte) {
this.name = name;
this.punkte = punkte;
}
feiereSieg() {console.log(`${this.name} feiert!`);}
}
Es ist extrem wichtig zu wissen: Das ist nur "syntaktischer Zucker"! Unter der Haube macht JavaScript exakt dasselbe wie im obigen Beispiel mit Spieler.prototype.feiereSieg. Die class-Syntax wurde nur eingeführt, um Entwicklern, die von Java oder C# kommen, den Einstieg zu erleichtern. JavaScript bleibt im Herzen eine prototypenbasierte Sprache.
prototype ist ein Speicherplatz, der an Konstruktor-Funktionen klebt. Alles, was du dort hineinsteckst, teilen sich alle Instanzen, die du mit new erzeugst.
Die Prototypen-Kette (Prototype Chain) ist der Suchpfad: Wenn ein Objekt eine Eigenschaft oder Methode selbst nicht hat, delegiert es die Suche rückwärts entlang seiner Ahnenreihe, bis es beim Ur-Objekt (Object.prototype) oder bei null ankommt.
Der prototypenbasierte Vererbungsmechnismus ist dynamisch. Ein Objekt erbt seine Eigenschaften von seinem Prototyp. Das geschieht auch dann, wenn Eigenschaften oder Methoden nach Erstellung des Objekts geändert werden. So hat man die Möglichkeit auch die eingebauten Javascript Klassen zu erweitern.
In diesem Beispiel wurde mittels einer Funktion ein Objekt erstellt. Nun hat man die Möglichkeit im Nachhinein Eigenschaften und Methoden anzuhängen und zwar mit der Schlüsselwort prototype. Das bedeutet im Prototyp des Konstruktors wird eine Methode erstellt.
Auto.prototype.aboutExtras = function(){ anweisung; anweisung;};
Die Konstruktorfunktion ist ein Funktion die aufgerufen wird, wenn man mit new ein neues Objekt erzeugt. Was gehört in die Konstruktorfunktion was nicht.
Genau das ist die goldene Regel für das klassische JavaScript-Design:
Eigenschaften (Daten wie Name, Punkte, Level) gehören in den Konstruktor. Methoden (Funktionen wie feiereSieg, laufen, berechneX) gehören an den Prototypen.
Aber schauen wir uns mal genau an, warum das im Speicher so einen gewaltigen Unterschied macht. Das Geheimnis liegt darin, wann und wie oft das Schlüsselwort function (oder die Arrow-Function) ausgeführt wird.
function Spieler(name, punkte) {
this.name = name;
this.punkte = punkte;
this.feiereSieg = function () { ... };
// <-- Hier wird jedes Mal Code ausgeführt!
}
Wenn du new Spieler("Anna", 100) aufrufst, passiert folgendes:
JavaScript erstellt ein brandneues, leeres Objekt im Arbeitsspeicher.
Es führt den Code im Konstruktor aus.
Bei der Zeile this.feiereSieg = function() { ... } sieht JavaScript das Wort function. Für die Engine bedeutet das: "Aha, erstelle im Speicher ein brandneues Funktions-Objekt!"
Wenn du jetzt 3 Spieler erstellst, sieht dein Speicher vereinfacht so aus:
[Speicheradresse #001] -> spieler1 = { name: "Anna", punkte: 100, feiereSieg: (Funktion #A) }
[Speicheradresse #002] -> spieler2 = { name: "Ben", punkte: 85, feiereSieg: (Funktion #B) }
[Speicheradresse #003] -> spieler3 = { name: "Chris",punkte: 50, feiereSieg: (Funktion #C) }
Obwohl Funktion #A, #B und #C exakt denselben Quellcode beinhalten, sind es für JavaScript drei völlig voneinander unabhängige Objekte im Speicher. Sie belegen dreimal Platz.
function Spieler(name, punkte) {
this.name = name;
this.punkte = punkte;
}
Spieler.prototype.feiereSieg = function () { ... };
Hier liest JavaScript die Datei von oben nach unten. Es sieht das Spieler.prototype.feiereSieg = function... nur ein einziges Mal beim Laden des Skripts. Es erstellt genau eine Funktion im Speicher und hängt sie an das prototype-Objekt von Spieler.
Wenn du jetzt 3 Spieler mit new erstellst:
JavaScript erstellt das leere Objekt.
Es kopiert nur name und punkte hinein.
Es gibt der Instanz einen unsichtbaren Link (__proto__), der auf Spieler.prototype zeigt.
Dein Speicher sieht jetzt so aus:
[Zentraler Prototyp] -> Spieler.prototype = { feiereSieg: (Zentrale Funktion #1) }
[Speicheradresse #001] -> spieler1 = { name: "Anna", punkte: 100, __proto__ -> zeigt auf Zentraler Prototyp }
[Speicheradresse #002] -> spieler2 = { name: "Ben", punkte: 85, __proto__ -> zeigt auf Zentraler Prototyp }
[Speicheradresse #003] -> spieler3 = { name: "Chris",punkte: 50, __proto__ -> zeigt auf Zentraler Prototyp }
Wenn du spieler1.feiereSieg() aufrufst, schaut JavaScript in spieler1. Da ist nichts. Also folgt es dem Link zu Spieler.prototype, findet dort die Zentrale Funktion #1 und führt sie aus.
Das Geniale: Für this.name nutzt JavaScript das Objekt, das die Funktion aufgerufen hat (also spieler1). Deshalb weiß die zentrale Funktion trotzdem, dass sie "Anna" ausgeben muss!
Ja, absolut! Zumindest alles, was eine Funktion (eine Methode) ist.
Es gibt allerdings zwei Ausnahmen, bei denen man Variablen oder Werte (keine Funktionen) doch nicht an den Prototypen hängt, selbst wenn sie nicht aus den Argumenten kommen:
Konstante Standardwerte: Wenn jeder Spieler am Anfang standardmäßig level = 1 und lebenspunkte = 100 hat, könntest du das theoretisch an den Prototypen hängen:
Spieler.prototype.level = 1;
Das spart zwar minimal Speicher, wird aber unübersichtlich. Moderne Entwickler schreiben solche Standardwerte meistens trotzdem in den Konstruktor (this.level = 1;), damit man auf einen Blick sieht, welche Eigenschaften ein Objekt hat.
Gefahr bei Arrays und Objekten (Referenztypen): Wenn du ein Array an den Prototypen hängst (z. B. Spieler.prototype.inventar = [];), dann teilen sich alle Spieler dasselbe Array! Wenn Anna ein Schwert aufhebt, hat Ben es plötzlich auch im Inventar. Das ist ein klassischer Bug.
Genau aus diesem Grund wurde dieses System erfunden. Und genau deshalb macht die moderne class-Syntax in JavaScript im Hintergrund auch automatisch genau das:
Wenn du in einer modernen Klasse eine Methode schreibst, packt JavaScript sie unter der Haube immer auf das prototype-Objekt, um Speicher zu sparen. Du hast das Prinzip also perfekt durchschaut!