Fortgeschrittene OO-Programmierung in Javascript

Prototypen

Prototypen sparen Speicher

Nachteilig an der direkten Definition von Methoden im Konstruktor ist, dass es pro Instanz von Person jeweils eine Instanz des Funktionsobjekts getName gibt. Bei vielen Instanzen bedeutet das einen unnötig hohen Speicherverbrauch, da gleiche Methoden mehrfach im Speicher gehalten werden. Um das zu vermeiden, kann man sich den sogenannten Prototypen zunutze machen, den jedes Javascript-Objekt besitzt. Er dient als Fallback: Wenn ein Objekt nach einem Attribut gefragt wird, das es nicht selbst besitzt, greift der Interpreter auf den Prototypen zurück. Ist es hier ebenfalls nicht zu finden, zieht er dessen Prototyp heran, was sich so lange wiederholt, bis er das Attribut gefunden hat oder das Ende der Prototyp-Kette erreicht ist.

Den Prototyp eines Objekts kann man nicht direkt setzen, sondern muss ihn indirekt über den Konstruktor festlegen: Bei jeder Erzeugung eines Objekts wird das prototype-Attribut seiner Konstruktor-Funktion abgefragt und sein Inhalt als Prototyp für das neue Objekt verwendet. Als Hilfestellung enthält das prototype-Attribut jeder Funktion schon per Default ein leeres Objekt, das man mit eigenen Werten (üblicherweise Funktionen) füllen kann. Listing 2 zeigt das anhand einer getName-Methode für alle Person-Objekte: Durch die Verwendung des prototype-Attributs ist diese Methode nur einmal vorhanden, während Listing 1 für jede Person-Instanz eine eigene getName-Instanz erzeugt.

Unter anderem lässt sich der Prototype-Ansatz dazu nutzen, eine Vererbungshierarchie aufzubauen. Eine weit verbreitete Technik hierzu ist die Verwendung einer Instanz der Superklasse als Prototyp für die Objekte der Subklasse (siehe Listing 3 ). Durch die automatische Attributauflösung über die Prototyp-Kette sind damit alle Methoden von Person in Employee-Instanzen ebenfalls zugänglich. Gleichzeitig kann man an die als Prototyp verwendete Person-Instanz neue Methoden anhängen, die danach in allen Employee-Objekten zur Verfügung stehen. Wichtig dabei: Es wird nicht Person selbst (als Konstruktor beziehungsweise Klasse) verändert, sondern eine Instanz von Person, die als Prototyp für alle Employee-Objekte dient.

Eine Superklassen-Instanz als Prototyp zu verwenden ist in Javascript zwar üblich, für Neulinge allerdings recht gewöhnungsbedürftig. Außerdem führt das zu sinnlosen Aufrufen des Konstruktors der Superklasse, was eigentlich nur sinnvoll ist, wenn es um die Erstellung richtiger[(i] Instanzen geht und nicht um den Aufbau der Vererbungshierarchie. Dieser konzeptionelle Bruch zeigt sich auch daran, dass in Listing 3 der Person-Konstruktor ohne Parameter aufgerufen wird, obwohl er einen Vor- und Nachnamen erwartet. Da man ja gar keine konkrete Person-Instanz erzeugen will, gäbe es hier auch keinen sinnvollen Wert, den man angeben könnte. Dass der Aufruf an sich dennoch nicht zu Fehlern führt, liegt daran, dass Javascript für alle nicht angegebenen Parameter automatisch [i]undefined einsetzt.

Bessere Vererbung

Um derlei zu vermeiden, behelfen sich viele Entwickler auf die eine oder andere Weise. Eine der elegantesten Lösungen ist, was man statt der Zeile Employee.prototype = new Person(); in Listing 3 verwenden kann:

var Temp = function() {};

Temp.prototype = Person.prototype;
Employee.prototype = new Temp();

Ein leerer temporärer Konstruktor dient der Erzeugung des Employee-Prototyps. Das vermeidet den Aufruf des Person-Konstruktors, während die Hierarchie durch das Setzen von Temp.prototype auf Person.prototype gewahrt bleibt.

Im Fall des Prototyp-Objekts ist es nützlich, den Aufruf des Superklassen-Konstruktors zu vermeiden. Ganz anders sieht es im Konstruktor der Unterklasse aus. Hier muss man sogar den Superklassen-Konstruktor aufrufen, um das Objekt korrekt zu initialisieren. In anderen Sprachen wie Java ist das recht einfach: super(); – Javascript kennt diese Syntax aber nicht. Man kann zwar die Konstruktor-Funktion der Superklasse direkt aufrufen (Person();), aber dann fehlt ihr der Kontext, das heißt this ist nicht korrekt gesetzt. Abhilfe schafft hier die sogenannte call-Methode, die jedes Funktionsobjekt in Javascript besitzt. Mit ihr kann man eine Funktion aufrufen und gleichzeitig bestimmen, auf welches Objekt das this-Schlüsselwort während des Aufrufs zeigen soll. Die Anwendung von call ist in Listing 3 am Konstruktor und der überschriebenen getName-Methode zu sehen. In beiden Fällen wird die aktuelle Instanz an die aufgerufene Funktion durchgereicht, indem sie als erster Parameter an call übergeben wird.

Von welchem Typ ein Objekt ist

Ein Punkt, der bisher unberücksichtigt blieb, ist die Frage nach Typinformationen. Oft muss man herausfinden können, ob ein konkretes Objekt eine Instanz einer bestimmten Klasse ist (oder allgemeiner: von einem bestimmten Typ). Generelle Typinformationen erhält man in Javascript mit dem typeof-Operator. typeof 5 liefert beispielsweise den String number. Allerdings lässt sich typeof nicht für Klassenhierarchien verwenden, denn es liefert für Objekte immer nur object zurück – gleichgültig, welche Konstruktor-Funktion sie erzeugt hat.

Generell gibt es in Javascript leider keine Möglichkeit, einem Objekt seine Klasse anzusehen. Es gibt zwar Konstruktionen, die mit dem constructor-Attribut arbeiten, das beim Erzeugen eines Objekts automatisch gesetzt wird, aber die versagen spätestens, wenn Namensräume zum Einsatz kommen (siehe unten) oder der Konstruktor aus einem anderen Grund nicht mehr dem Schema function <Name>(<Parameter>) entspricht.

Was allerdings funktioniert, ist der instanceof-Operator. Mit ihm lässt sich feststellen, ob der Wert des prototype-Attributs einer Funktion in der Prototyp-Kette eines Objekts vorkommt. Klingt kompliziert, bedeutet aber in der Praxis nichts anderes, als dass man feststellen kann, ob ein Objekt zu einer Klasse oder einer ihrer Subklassen gehört (siehe Listing 4).