Pseudoklassische Vererbung in JavaScript – Teil 1
Note: This post is from my old blog and thus written in German and potentially obsolete.
Bekanntermaßen gibt es in JavaScript zwar keine Klassen, die Sprache ist auf prototypische Delegation ausgerichtet, der Einsatz von Konstruktor-Funktionen zusammen mit dem new
-Operator erinnert jedoch stark an den Umgang mit Klassen in Programmiersprachen wie z.B. Java. Man spricht hier deshalb u.a. von einer pseudoklassischen Vererbung.
Stoyan Stefanov beschreibt in seinem Buch “JavaScript Patterns” fünf Herangehensweisen an diese Art von Vererbung, die ich in jeweils einem Post hier vorstellen möchte.
1. Das Standardmuster
Kenntnisse über die Funktionsweise von Konstruktor-Funktionen und des Prototyps sind Voraussetzung für das Verständnis dieses und der folgenden Muster.
Neben dem Objekt-Literal dienen in JavaScript auch Konstruktor-Funktionen zum Erzeugen von Objekten. Hier ein einfaches Beispiel aus “JavaScript Patterns”
var Person = function (name) {
this.name = name;
this.say = function () {
return "Ich bin " + this.name;
};
};
Wenn man diese Funktion nun mit vorangestelltem new
-Operator aufruft (warum dieser notwendig ist erkläre ich ein Stück weiter unten), passieren einige Dinge implizit, die im Folgenden durch zusätzliche Kommentare innerhalb der Funktion dargestellt werden:
var Person = function (name) {
// var this = {};
this.name = name;
this.say = function () {
return "Ich bin " + this.name;
};
// return this;
};
var me = new Person();
Es wird ein leeres Objekt erzeugt und über this
referenziert. Dann werden ihm eine name
-Eigenschaften und say
-Methode hinzugefügt. Zum Schluss erfolgt die Rückgabe des Objekts mittels return. Das Objekt wird dabei in diesem Fall der Variable Person zugewiesen.
Jede JavaScript Funktion die man erzeugt erhält außerdem automatisch eine prototype
-Eigenschaft, die auf ein (fast) leeres Objekt zeigt. Es enthält bereits eine constructor
-Eigenschaft, welche wiederum auf die Funktion zeigt, durch die es erzeugt wurde.
Wenn man dem Prototyp eines Konstruktors Eigenschaften und Methoden hinzufügt, stehen diese auch den vom Konstruktor erzeugten Objekten zur Verfügung. Da Objekte in JavaScript als Referenz übergeben werden, sind Änderungen am Prototyp sofort für alle Objekte sichtbar, die auf ihn zugreifen. Er eignet sich bei der Vererbung daher sehr gut für Eigenschaften die über alle Instanzen hinweg gleich sind, und nicht bei jeder Instanziierung neu erzeugt werden müssen.
Ausführlichere Informationen zum Thema Prototyp findet man u.a. in [diesem Artikel](https:// molily.de/js/organisation-instanzen.html#prototypen-verstehen) von Matthias Schäfer (molily).
Nun zum eigentlichen Standardmuster. Hier wird ein Objekt mit Hilfe eines Eltern-Konstruktors erzeugt, und dieses Objekt dem Prototyp eines Kindes zugewiesen.
// Eltern-Konstruktor
function Parent(name) {
this.name = name || "Tom";
}
// Die Methode say ist für alle Instanzen gleich, deshalb wird sie dem Prototyp hinzugefügt
Parent.prototype.say = function () {
return this.name;
};
// leerer Kind-Konstruktor
function Child(name) {}
// Standardmuster für die Vererbung
function inherit(Child, Parent) {
// neue, durch den Eltern-Konstruktor erzeugte Objekte werden dem Prototyp des Kind-Konstruktors zugewiesen
Child.prototype = new Parent();
}
inherit(Child, Parent);
var son = new Child();
son.say(); // "Tom"
Wenn man nun den Kind-Konstruktor benutzt um ein Objekt zu erzeugen, hier im Beispiel der Variable son
zugewiesen, erbt es über seinen Prototyp (Child.prototype
) die Eigenschaften der erzeugten Instanz des Eltern-Konstruktors (new Parent();
).
Wichtig hierbei ist es wie gesagt jeweils den new
-Operator zu Erzeugung des neuen Objekts zu verwenden, weil das this
-Schlüsselwort innerhalb der Konstruktoren ansonsten nicht auf das neue sondern das globale Objekt zeigt, innerhalb eines Browsers also auf window
. Somit würde man mit obigem Beispielcode durch Verwenden von Child.prototype = Parent();
dem window Objekt eine neue Eigenschaft name
hinzufügen (this.name
), und die erwünschte Vererbung von Eigenschaften verhindern.
Wie im Beispielcode zu sehen, wird die Funktion say aufgerufen als wäre sie innerhalb des Kind-Konstruktors definiert worden (son.say();
). Dies ist jedoch nicht der Fall. Hier kommt die sogenannte Prototyp-Kette (prototype chain) ins Spiel, welche durch den Code der inherit Funktion initiiert wird, und die Vererbung der Eigenschaften ermöglicht.
Da son
über keine eigene say
-Methode verfügt, schaut es in seiner prototype
-Eigenschaft nach, in der sich nicht mehr das standardmäßig vorhandene, leere Objekt befindet, sondern die Instanz des Eltern-Konstruktors mit der es überschrieben wurde (Child.prototype = new Parent();
). Diese enthält jedoch nur eine name
-Eigenschaft. Deshalb wird im nächsten Schritt der Prototyp des Eltern-Konstruktors durchsucht. Und dort wird son
fündig, da Parent.prototype.say
im Beispielcode definiert wurde.
Dies ist zwar eine recht knappe Erklärung, sie sollte jedoch ausreichen um zu verstehen, wie die Prototyp-Kette Vererbung ermöglicht.
Nachteile des Standarsmusters
Bei Verwendung dieses Muster werden sowohl Eigenschaften welche this hinzugefügt wurden, als auch die Eigenschaften des Prototyps vererbt. Meistens dürften jedoch nur letztere von Interesse sein, da die Eigenschaften von this oftmals instanzspezifisch sind.
Außerdem kann man dem Kind-Konstruktor keine Parameter übergeben, welche dann an den Eltern-Konstruktor weitergereicht werden. Im Beispielcode akzeptiert der Eltern-Konstruktor den name
-Parameter. Ruft man den Kind-Konstruktor jedoch auf, und übergibt ihm einen Wert für name
, wird dieser nicht vom Eltern-Konstruktor übernommen:
var son = new Child("Alex");
son.say(); // "Tom"