JavaScript Coding Patterns: Objekt-Vererbung mit Prototypen

In den vorangegangenen Teilen der Reihe JavaScript Coding Patterns (Teil I, Teil II und Teil III) wurde immer wieder betont, dass JavaScript eine funktionale und zugleich objektorientierte Skriptsprache ist. Funktionen sind hier selbst Objekte die in JavaScript nicht auf Klassen beruhen, sondern ihre Attribute und Methoden von bereits existierenden Objekten erben. Es handelt sich dabei um eine prototypenbasierte Art der Vererbung.

In diesem vierten Teil der Reihe wird es darum gehen sich die Rolle, die Prototypen in JavaScript spielen, etwas genauer anzuschauen.

Objekte erzeugen

Die einfachste Art ein Objekt in JavaScript zu erzeugen ist sicherlich die Definition per Objekt-Literal. Im Rahmen einer bestimmen Syntax werden so die Attribute eines Objekts als Schlüssel-Wert-Paare beschrieben:

Der lesende und schreibende Zugriff auf die einzelnen Objekt-Attribute erfolgt dann mit der bekannten Punkt-Notation:

Eine Alternative zu den Objekt-Literalen ist die Verwendung der nativen Konstruktor-Funktion Object(). Im Zusammenspiel mit dem new-Operator gestaltet sich die Objekt-Erzeugung dann folgendermaßen:

Damit ist zunächst nur ein leeres Objekt angelegt worden, dessen Attribute nachträglich befüllt werden können:

Konstruktor-Funktionen und der new-Operator

Der Nachteil der beiden oben gezeigten Methoden zur Objekterzeugung ist nicht schwer zu erkennen: In beiden Fällen kann das Customer-Objekt nicht wiederverwendet werden. Um einen weiteren customer anzulegen müsste sowohl ein weiteres Objekt-Literal als auch ein neues Objekt per nativer Konstruktor-Funktion erstellt werden.

Selbst definierte Konstruktoren
Die Lösung besteht darin, eine eigene Konstruktor-Funktion zu definieren:

Dadurch wird es möglich verschiedene Objekte mit nur einer Konstruktor-Funktion zu erzeugen. Einer der großen Vorteile einer Funktion ist schließlich die Möglichkeit Parameter entgegenzunehmen. Und so werden der Name und die E-Mail-Adresse eines Customers als Argumente in den Konstruktor hineingereicht und als Werte für die entsprechenden Attribute gesetzt:

Es sollte auch aufgefallen sein, dass innerhalb der Konstruktor-Funktion die Eigenschaften name und email nicht als einfache Variablen, sondern als Attribute von this definiert werden. Dies ist nur durch die Verwendung des new-Operators beim Aufruf des Konstruktors möglich. Hinter den Kulissen bewirkt der new-Operator das Speichern des aktuellen Objekts in der Variablen this. Außerdem wird das so angereicherte Objekt this automatisch als Rückgabewert der Konstruktor-Funktion gesetzt. Darum ist es nicht nötig und in den allermeisten Fällen auch gar nicht erwünscht, einen eigenen Rückgabewert zu bestimmen.

Nachteile des new-Operators
Allerdings hat die Verwendung eines Konstruktors im Zusammenhang mit dem new-Operator auch einen Nachteil. Nämlich den, dass die Konstruktor-Funktion eben keine Klasse darstellt, die bereits vor Beginn der Laufzeit definiert wäre. Es handelt sich stattdessen immer noch um eine einfache Funktion. Also kann die Funktion Customer() auch ohne den new-Operator aufgerufen werden. Das führt nicht zu einem Fehler, hat aber trotzdem weitreichende Konsequenzen. Das nächste Beispiel soll dies zeigen:

Die Funktion TestFunction(), die lediglich this auf der Browser-Konsole ausgibt, wird einmal mit und einmal ohne den new-Operator aufgerufen. Im ersten Fall beinhaltet this eine Instanz von TestFunction. Aber im zweiten Fall, wenn die Funktion ohne new aufgerufen wird, hat sie ihren lokalen Scope verloren und ist dem globalen Scope zugeordnet worden: this zeigt darum folgerichtig auf das Window-Objekt.

Namenskonventionen und Self-Invoking Constructors
Um eine Funktion als Konstruktor erkennbar zu machen hat sich eine Namenskonvention durchgesetzt: Man orientiert sich an den nativen JavaScript-Konstruktoren und lässt darum auch die Namen der selbst definierten Konstruktoren mit einem Großbuchstaben beginnen.

Zusätzlich kann programmatisch die Verwendung des new-Operators erzwungen werden, z.B. durch die Implementierung eines sich selbst aufrufenden Konstruktors. Im Fall des Customer-Konstruktors sähe der Code folgendermaßen aus:

Sowohl mit als auch ohne Verwendung des new-Operators zeigt this jetzt immer auf die Instanz eines Customer-Objektes.

Prototypen und Vererbung

Hat man die Verwendung einer JavaScript-Funktion als Objekt-Konstruktor im Zusammenhang mit dem Einsatz des new-Operators verstanden, so ist die Einsicht in die Funktionsweise von Prototypen nur noch ein relativ kleiner Schritt.

Objekte erben von Objekten
Jede Funktion in JavaScript hat eine Eigenschaft mit dem Namen prototype, welche auf ein Objekt zeigt. Wenn nun eine Funktion, wie oben gezeigt, mit Hilfe des new-Operators aufgerufen wird, dann wird gleichzeitig ein neues Objekt erzeugt, genauer gesagt, es wird geklont, und von der Funktion zurückgeliefert. Das neue Objekt, der Klon, verfügt danach weiterhin über eine Verlinkung zum Prototyp-Objekt seiner Konstruktor-Funktion. Die besagte Verlinkung wird z.B. in vielen Browsern als __proto__ bezeichnet und gelistet.

Das Prototyp-Objekt selbst ist ein normales JavaScript-Objekt. Darin können Attribute und Methoden gespeichert werden, auf die auch das neu erzeugte Objekt so zugreifen kann, als ob sie unmittelbar Eigenschaften von ihm selbst sind. Und genau darin besteht die Besonderheit von Objekt-Vererbung in JavaScript: Konkrete Objekte teilen sich Attribute und Methoden, indem sie Referenzen auf wiederum konkrete Objekte aufrechterhalten, die diese Attribute und Methoden besitzen.

Verkettete Prototypen
Da alle Objekte in JavaScript eine Verlinkung zu ihrem Prototyp-Objekt besitzen, trifft dies auch auf das Attribut prototype zu. So gesehen ergibt sich eine Kette, die sogenannte prototype chain:

The-JavaScript-Prototype-Chain

Die Illustration zeigt ein Objekt A, das eine Reihe von Eigenschaften besitzt. Eine dieser Eigenschaften ist die Eigenschaft __proto__, ein Link zu einem weiteren Objekt, in diesem Fall dem Objekt B. Und B wiederum verfügt über eine Verknüpfung zu Objekt C. Die Kette endet mit dem Prototyp-Objekt des nativen Object, der „Mutter“ aller JavaScript-Objekte.

Ein Beispiel
Praktisch sehr deutlich und relevant wird die Vererbung über Prototypen dann, wenn ein Objekt in der Vererbungshierarchie z.B. eine bestimmte Methode selbst nicht hat, aber ein anderes Objekt in der Kette über diese Methode verfügt. In diesem Fall kann das erste Objekt auf die gesuchte Methode einfach zugreifen, als ob es seine eigene wäre. Die folgenden Code-Beispiele skizzieren ein solches Szenario.

Als Erstes wird die Konstruktor-Funktion User() definiert und dann zwei Methoden im Prototyp-Objekt dieses Konstruktors abgelegt:

Im nächsten Schritt wird eine weitere Konstruktor-Funktion definiert. Die mit diesem Konstruktor erzeugten Customer-Objekte sollen später von den User-Objekten erben:

Nun folgt der Teil des Codes, der dafür verantwortlich ist, dass ein Customer-Objekt auf das Prototype-Objekt eines User zugreifen kann:

Die Eigenschaft prototype des Customer-Konstruktors wird komplett mit einem neuen User-Objekt überschrieben. Damit sind alle Attribute und Methoden eines User-Objektes auch für das Customer-Objekt zugänglich. Mit der zweiten Code-Zeile wird die Eigenschaft constructor, ein Teil des Prototyp-Objektes, wieder zurückgesetzt, und zwar auf den Customer-Konstruktor.

Zum Schluss werden zwei Objekte mit den zuvor definierten Konstruktoren erzeugt, um zu sehen, ob dieser kleine Vererbungstest erfolgreich war:

Tatsächlich kann das Objekt john, das mit dem Customer-Konstruktor generiert wurde, sowohl auf die Methode setIp() als auch auf setLastVisit() zugreifen. Bei beiden handelt es sich um Methoden, die nur als Eigenschaften des User-Prototypen gesetzt wurden. Die prototype chain funktioniert!

Außerdem zeigt die Prüfung mit dem instanceof-Operator, dass das Objekt john korrekterweise beides ist: Eine Instanz von User und eine Instanz von Customer.

Fazit

So funktioniert Vererbung in JavaScript: Ein Objekt hat Zugriff auf jede Eigenschaft oder Funktion, die es entlang der Prototypen-Kette findet. Das ermöglicht einerseits sehr flexible Objekt-Strukturen. Andererseits kann es schnell zu unübersichtlichen Beziehungen zwischen den Objekten einer JavaScript-Applikation kommen.

Es stellt sich aber die Frage, in welchem Ausmaß ein heutiger JavaScript-Entwickler überhaupt noch komplexe Vererbungsstrukturen auf der Basis von Prototypen zu entwickeln hat. Man kann wohl eher davon ausgehen, dass derartige Aufgaben im Rahmen von JavaScript-Bibliotheken- und Frameworks gelöst werden, so dass es am Ende des Tages einfach nur gut ist zu wissen, wie prototypenbasierte Vererbung theoretisch funktioniert.

Weiter lesen und lernen!

Für neue Blogupdates anmelden:


7 Gedanken zu “JavaScript Coding Patterns: Objekt-Vererbung mit Prototypen

  1. Hallo,
    danke für den aufschlussreichen Artikel!

    kurze Frage jedoch:
    warum muss ich hier die zu vererbenden Funktionen im Prototypen des Objekts definieren:

    var User = function () {};

    User.prototype.setIp = function (ip) {
    this.ip = ip;
    };

    wäre es nicht auch ob diese Funktionen auf this zu legen. Also in etwa so:

    var User = function () {
    this.setIp = function (ip) {
    this.ip = ip;
    }
    };

    • Ersteres legt eine Methode im Prototype ab, welche bei weiterer Vererbung für alle Objekte in der nachfolgenden Prototype-Kette verfügbar ist.

      Dein Vorschlag legt eine Methode in jeder Instanz ab (durch den Contructor).
      Wölltest du im Nachhinein diese Methode überschreiben wollen, wird es so nicht mehr funktionieren und die alte Methode wird weiterhin aufgerufen. Siehe: https://jsfiddle.net/szurfh3k/

      • //Update2:
        Darüber hinaus kommt noch unabhängig der Vererbung noch die Themativ priviligierte VS nicht-priviligierte Methoden dazu. Eine per prototype definierte, nicht-priviligierte Methode kann nur auf öffentliche Eigenschaften und Methoden zugreifen.
        Die über this definierte, priviligierte Methode hat Zugriff auf alles innerhalb der Klasse. Das kann von Nutzem sein, bringt aber auch Sicherheitsrisiken mit – das ist bei der Entwicklung dann stets zu bedenken.

        Stichwort: public / private / privileged – falls man sich näher damit beschäftigen möchte

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.