Objekte sind die grundlegende Datenstruktur von JavaScript. Einfach ausgedrückt, stellt ein Objekt eine Liste dar, die Strings zu Werten zuordnet (Key/Value-Paare). Wenn Sie etwas genauer hinsehen, können Sie jedoch erkennen, dass Objekte sehr viel mehr Möglichkeiten enthalten.
Wie viele objektorientierte Sprachen bietet auch JavaScript Unterstützung für die Vererbung von Implementierungen, also die Wiederverwendung von Code und Daten. Anders als bei vielen konventionellen Sprachen basiert der Vererbungsmechanismus von JavaScript jedoch nicht auf Klassen, sondern auf dem Konzept von Prototypen. Daher ist JavaScript für viele Programmierer die erste objektorientierte Sprache ohne Klassen.
In vielen Sprachen ist jedes Objekt eine Instanz einer zugehörigen Klasse, die den gemeinsamen Code für alle ihre Instanzen bereitstellt. In JavaScript dagegen gibt es von Haus aus keine Klassen. Hier erben Objekte von anderen Objekten. Jedes Objekt ist mit einem anderen Objekt verbunden, seinem sogenannten Prototyp. Mit Prototypen müssen Sie auf andere Weise arbeiten als mit Klassen, wobei jedoch viele Prinzipien der herkömmlichen objektorientierten Sprachen nach wie vor Gültigkeit haben.
Für Prototypen gibt es drei miteinander verwandte, aber unterschiedliche Zugriffsmethoden, deren Namen leider alle irgendeine Variante des Worts »Prototyp« enthalten, was eine gewisse Verwechslungsgefahr heraufbeschwört. Sorgen wir also für Klärung:
Um diese drei Varianten besser zu verstehen, stellen Sie sich am besten die typische Definition eines JavaScript-Datentyps vor. Der Konstruktor User erwartet, mit dem Operator new aufgerufen zu werden, nimmt einen Namen und den Hashwert eines Passwortstrings entgegen und speichert sie in der neuen Instanz des User-Objekts.
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
User.prototype.toString = function() {
return "[User " + this.name + "]";
};
User.prototype.checkPassword = function(password) {
return hash(password) === this.passwordHash;
};
var u = new User("sfalken", "0ef33ae791068ec64b502d6cb0191387");
prototype
Die Funktion User weist die Standardeigenschaft prototype auf, die wiederum ein Objekt enthält, das zu Anfang mehr oder weniger leer ist. In diesem Beispiel fügen wir dem Objekt User.prototype die beiden Methoden toString und checkPassword hinzu. Wenn wir mit dem Operator new eine Instanz von User bilden, wird dem resultierenden Objekt u automatisch das in User.prototype gespeicherte Objekt als Prototyp zugewiesen. Abbildung 4–1 zeigt ein Diagramm dieser Objekte.
Beachten Sie den Pfeil, der das Instanzobjekt u mit dem Prototypobjekt User.prototype verbindet und die Vererbungsbeziehung darstellt. Wenn Eigenschaften nachgeschlagen werden, beginnt die Suche erst in den eigenen Eigenschaften des Objekts. So geben beispielsweise u.name und u.passwordHash die aktuellen Werte der betreffenden Eigenschaften von u selbst zurück. Eigenschaften, die nicht direkt in u zu finden sind, werden im Prototyp von u nachgeschlagen. Beim Zugriff auf u.checkPassword beispielsweise wird eine in User.prototype gespeicherte Methode abgerufen.
Abb. 4–1 Prototypbeziehungen zwischen dem Konstruktor User und der Instanz
getPrototypeOf()
Das führt uns zum nächsten Punkt auf unserer Liste. Die Eigenschaft prototype einer Konstruktorfunktion richtet die Prototypbeziehung neuer Instanzen ein; die ES5-Funktion Object.getPrototypeOf() dagegen ruft den Prototyp eines bestehenden Objekts ab. Nachdem wir in dem obenstehenden Beispiel u erstellt haben, können wir nun folgenden Test durchführen:
Object.getPrototypeOf(u) === User.prototype; // true
__proto__
Manche Umgebungen stellen mithilfe der besonderen Eigenschaft __proto__ einen nicht standardkonformen Mechanismus bereit, um den Prototyp eines Objekts abzurufen. Das kann eine nützliche Zwischenlösung für Umgebungen sein, die die ES5-Funktion Object.getPrototypeOf noch nicht unterstützen. In solchen Umgebungen können wir einen ähnlichen Test durchführen:
u.__proto__ === User.prototype; // true
Klassen
Noch ein letztes Wort zu Prototypbeziehungen: JavaScript-Programmierer bezeichnen User häufig als eine Klasse, auch wenn sie kaum mehr umfasst als eine Funktion. Klassen in JavaScript sind im Grunde genommen eine Kombination aus einer Konstruktorfunktion (User) und einem Prototypobjekt (User.prototype), mit dessen Hilfe die Methoden von allen Instanzen der Klasse gemeinsam genutzt werden können.
Abb. 4–2 Prinzipielle Darstellung der »Klasse« User
Abbildung 4–2 vermittelt eine gute prinzipielle Vorstellung von der Klasse User. Die Funktion User stellt einen öffentlichen Konstruktor für die Klasse bereit, und User.prototype ist eine interne Implementierung der Methoden, die die Instanzen gemeinsam nutzen. Für die üblichen Benutzer von User und u ist es nicht erforderlich, direkt auf das Prototypobjekt zuzugreifen.
getPrototypeOf() ist Standard.
In ES5 wurde Object.getPrototypeOf als Standard-API für den Abruf des Prototyps eines Objekts eingeführt – allerdings erst, nachdem eine Reihe von JavaScript-Engines schon seit längerer Zeit die besondere Eigenschaft __proto__ für denselben Zweck zur Verfügung gestellt hatten. Diese Erweiterung wird jedoch nicht von allen JavaScript-Umgebungen unterstützt, und in den anderen ist die Verwendung nicht vollständig kompatibel. Beispielsweise unterscheiden sich die Umgebungen in ihrer Handhabung von Objekten mit dem Prototyp null. In einigen Umgebungen erbt __proto__ von Object.prototype, weshalb ein Objekt mit dem Prototyp null nicht über die Sondereigenschaft __proto__ verfügt:
var empty = Object.create(null); // Objekt ohne Prototyp
"__proto__" in empty; // false (in einigen Umgebungen)
In anderen Umgebungen dagegen wird __proto__ unabhängig vom Zustand des Objekts immer auf besondere Weise gehandhabt:
var empty = Object.create(null); // Objekt ohne Prototyp
"__proto__" in empty; // true (in einigen Umgebungen)
Wenn Object.getPrototypeOf verfügbar ist, steht Ihnen damit eine standardkonforme und weiträumiger portierbare Möglichkeit zum Abruf des Prototyps zur Verfügung. Vor allem aber verschmutzt __proto__ alle Objekte (siehe Thema 45) und führt daher zu einer ganzen Reihe von Bugs. JavaScript-Engines, die diese Erweiterung zurzeit noch unterstützen, werden daher in späteren Versionen möglicherweise zulassen, dass sie von Programmen deaktiviert wird, um diese Fehler zu vermeiden. Wenn Sie Object.getPrototypeOf verwenden, wird Ihr Code auch dann noch funktionieren, wenn __proto__ deaktiviert ist.
getPrototypeOf selbst implementieren
In JavaScript-Umgebungen, die die ES5-API nicht bereitstellen, können Sie sie mithilfe von __proto__ auf einfache Weise selbst implementieren:
if (typeof Object.getPrototypeOf === "undefined") {
Object.getPrototypeOf = function(obj) {
var t = typeof obj;
if (!obj || (t !== "object" && t !== "function")) {
throw new TypeError("not an object");
}
return obj.__proto__;
};
}
Diese Implementierung können Sie auch in ES5-Umgebungen gefahrlos einbauen, da die Funktion gar nicht erst installiert wird, wenn Object.getPrototypeOf bereits vorhanden ist.
Portabilität
Die besondere Eigenschaft __proto__ bietet noch eine zusätzliche Möglichkeit, die Object.getPrototypeOf nicht hat, nämlich die Fähigkeit, die Prototypbeziehung eines Objekts zu ändern. Das mag sich zwar harmlos anhören (was ist schließlich schon ein anderer Prototyp?), zieht aber ernste Konsequenzen nach sich und sollte vermieden werden. Der offensichtlichste Grund, um auf die Änderung von __proto__ zu verzichten, ist die Portabilität: Da nicht alle Plattformen die Möglichkeit unterstützen, den Prototyp eines Objekts zu ändern, kann Code, der dies tut, niemals vollständig portierbar sein.
Leistung
Ein weiterer Grund, der gegen eine Änderung von __proto__ spricht, ist die Leistung. Alle modernen JavaScript-Engines optimieren den Abruf und die Festlegung von Objekteigenschaften in großem Maßstab, da dies zu den häufigsten Operationen gehört, die JavaScript-Programme durchführen. Diese Optimierungen aber basieren auf den Kenntnissen der Engine über die Struktur eines Objekts. Wenn Sie die innere Struktur eines Objekts ändern, indem Sie beispielsweise Eigenschaften zu ihm oder zu einem anderen Objekt in seiner Prototypkette hinzufügen oder daraus entfernen, dann werden einige dieser Optimierungen unwirksam. Eine Bearbeitung von __proto__ ändert die Vererbungsstruktur, und das ist die destruktivste Änderung, die überhaupt möglich ist. Damit können weit mehr Optimierungen zunichte gemacht werden als durch Änderungen an normalen Eigenschaften.
Vorhersagbarkeit
Der wichtigste Grund dafür, auf eine Änderung von __proto__ zu verzichten, besteht jedoch darin, ein vorhersagbares Verhalten zu wahren. Das Verhalten eines Objekts wird durch seine Prototypkette bestimmt, da sie seine Menge an Eigenschaften und Eigenschaftswerten festlegt. Die Änderung der Prototypbeziehung eines Objekts ist im Grunde genommen eine Gehirntransplantation: Die gesamte Vererbungshierarchie des Objekts wird ausgetauscht. Möglicherweise gibt es Ausnahmefälle, in denen eine solche Operation sinnvoll ist, aber der gesunde Menschenverstand sollte einem schon sagen, dass man die Vererbungshierarchie lieber unangetastet lässt.
Object.create
Um neue Objekte mit einer maßgeschneiderten Prototypbeziehung zu erstellen, können Sie in ES5 Object.create verwenden. Für Umgebungen, die ES5 nicht implementieren, bietet Thema 33 eine portierbare Implementierung von Object.create, die sich nicht auf __proto__ stützt.
Wenn Sie einen Konstruktor wie die Funktion User aus Thema 30 erstellen, müssen Sie sich darauf verlassen, dass er immer mit dem Operator new aufgerufen wird. Beachten Sie, dass die Funktion davon ausgeht, dass der Empfänger ein brandneues Objekt ist:
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
}
Wenn der Aufrufer das Schlüsselwort new versehentlich nicht angibt, wird das globale Objekt zum Empfänger der Funktion:
var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
u; // undefined
this.name; // "baravelli"
this.passwordHash; // "d8b74df393528d51cd19980ae0aa028e"
Das führt nicht nur dazu, dass die Funktion undefined zurückgibt, obwohl es nicht nötig wäre, sondern sie erstellt auch die globalen Variablen name und passwordHash (oder ändert sie, falls sie bereits vorhanden waren) – und das ist wirklich katastrophal!
Ist die Funktion als ES5-Code im Strict Mode definiert, lautet der Standardempfänger undefined:
function User(name, passwordHash) {
"use strict";
this.name = name;
this.passwordHash = passwordHash;
}
var u = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
// Fehler: this ist undefined.
In diesem Fall führt der problematische Aufruf sofort zu einem Fehler: Die erste Zeile von User versucht eine Zuweisung zu this.name durchzuführen, was einen TypeError-Fehler auslöst. Wurde in der Konstruktorfunktion der Strict Mode gesetzt, kann der Aufrufer den Bug zumindest schnell erkennen und korrigieren.
In jedem Fall aber ist die Funktion User instabil. Wenn sie mit new aufgerufen wird, funktioniert sie wie erwartet, aber bei einem Aufruf als normale Funktion schlägt sie fehl. Eine sicherere Vorgehensweise besteht darin, eine Funktion zu schreiben, die unabhängig von der Art ihres Aufrufs als Konstruktor wirkt. Eine einfache Möglichkeit dazu besteht darin, zu prüfen, ob der Empfängerwert eine gültige Instanz von User ist:
function User(name, passwordHash) {
if (!(this instanceof User)) {
return new User(name, passwordHash);
}
this.name = name;
this.passwordHash = passwordHash;
}
Auf diese Weise führt der Aufruf von User zu einem Objekt, das von User.prototype erbt, und zwar unabhängig davon, ob sie als Funktion oder als Konstruktor aufgerufen wird:
var x = User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
var y = new User("baravelli", "d8b74df393528d51cd19980ae0aa028e");
x instanceof User; // true
y instanceof User; // true
Ein Nachteil dieser Technik besteht jedoch darin, dass dafür ein zusätzlicher Funktionsaufruf erforderlich ist, was sie etwas teurer macht. Außerdem lässt sie sich schwer für variadische Funktionen gebrauchen (siehe Thema 21 und 22), da es kein einfaches Gegenstück zur Methode apply für den Aufruf variadischer Funktionen als Konstruktoren gibt.
Object.create
Es gibt jedoch die folgende, etwas exotisch anmutende Vorgehensweise, die die ES5-Funktion Object.create nutzt:
function User(name, passwordHash) {
var self = this instanceof User
? this
: Object.create(User.prototype);
self.name = name;
self.passwordHash = passwordHash;
return self;
}
Object.create nimmt ein Prototypobjekt entgegen und gibt ein neues Objekt zurück, das von diesem Prototyp erbt. Wenn diese Version von User als Funktion aufgerufen wird, ergibt sich daher ein neues Objekt, das von User.prototype erbt und in dem die Eigenschaften name und passwordHash initialisiert sind.
Zwar ist Object.create nur in ES5 verfügbar, doch können Sie in älteren Umgebungen etwas Ähnliches tun, indem Sie einen lokalen Konstruktor erstellen und mit new instanziieren:
if (typeof Object.create === "undefined") {
Object.create = function(prototype) {
function C() { }
C.prototype = prototype;
return new C();
};
}
(Beachten Sie, dass hiermit nur eine Version von Object.create mit einem einzigen Argument implementiert wird. Die echte Version nimmt auch ein optionales zweites Argument an, das einen Satz von Eigenschaftsbeschreibungen zur Definition des neuen Objekts angibt.)
Konstruktorüberschreibung
Was aber geschieht, wenn jemand diese neue Version von User mit new aufruft? Dank der Technik der Konstruktorüberschreibung (Constructor Override) verhält sie sich genauso wie bei einem Funktionsaufruf. Das liegt daran, dass es in JavaScript zulässig ist, das Ergebnis eines new-Ausdrucks mit einem ausdrücklichen return einer Konstruktorfunktion zu überschreiben. Wenn User den Wert self zurückgibt, erhält der new-Ausdruck das Ergebnis self, was ein anderes Objekt sein kann als das an this gebundene.
Einen Konstruktor vor der falschen Verwendung zu schützen, ist nicht immer die Mühe wert, vor allem, wenn Sie den Konstruktor nur lokal einsetzen. Trotzdem ist es wichtig zu wissen, welche schlimmen Auswirkungen es haben kann, wenn ein Konstruktor auf die falsche Weise aufgerufen wird. Zumindest sollten Sie die Stellen dokumentieren, an denen eine Konstruktorfunktion erwartet, mit new aufgerufen zu werden, vor allem wenn sie in umfangreichem Code oder in einer gemeinsamen Bibliothek steht.
Es ist durchaus möglich, JavaScript-Programme ohne Prototypen zu schreiben. Die Klasse User aus Thema 30 könnten wir wie folgt definieren, ohne irgendetwas Besonderes in ihrem Prototyp zu definieren:
function User(name, passwordHash) {
this.name = name;
this.passwordHash = passwordHash;
this.toString = function() {
return "[User " + this.name + "]";
};
this.checkPassword = function(password) {
return hash(password) === this.passwordHash;
};
}
In den meisten Verwendungszwecken verhält sich diese Klasse genauso wie die ursprüngliche Implementierung. Wenn wir aber mehrere Instanzen von User erstellen, zeigt sich ein entscheidender Unterschied:
var u1 = new User(/* ... */);
var u2 = new User(/* ... */);
var u3 = new User(/* ... */)
Abbildung 4–3 zeigt, wie diese drei Objekte und ihr Prototypobjekt aussehen. Anstatt die Methoden toString und checkPassword des Prototyps gemeinsam zu nutzen, enthält jede Instanz ein Exemplar dieser beiden Methoden, was insgesamt sechs Funktionsobjekte ergibt.
In Abbildung 4–4 können Sie sehen, wie diese drei Objekte und ihr Prototypobjekt in der ursprünglichen Definition aussehen. Hier werden die Methoden toString und checkPassword nur einmal erstellt und dann über den Prototyp von allen Instanzen gemeinsam verwendet.
Abb. 4–3 Methoden in Instanzobjekten speichern
Abb. 4–4 Methoden in Prototypobjekten speichern
Wenn Sie Methoden per Prototyp speichern, stehen diese allen Instanzen zur Verfügung, ohne dass mehrere Exemplare der implementierenden Funktionen oder zusätzliche Eigenschaften in den einzelnen Instanzobjekten erforderlich wären. Man könnte glauben, dass die Speicherung von Methoden in den Instanzobjekten die Geschwindigkeit von Nachschlagevorgängen für Methoden wie u3.toString() erhöht, da nicht die ganze Prototypkette nach der Implementierung von toString abgesucht werden muss. Allerdings wird das Nachschlagen von Methoden in modernen JavaScript-Engines sehr stark optimiert, weshalb das Kopieren der Methoden in die einzelnen Instanzobjekte nicht unbedingt merkbare Geschwindigkeitsverbesserungen nach sich zieht. Dagegen verschlingen Instanzmethoden aber fast immer mehr Arbeitsspeicher als Prototypmethoden.
Das Objektsystem von JavaScript trägt nicht gerade dazu bei, Informationen zu verbergen. Die Namen aller Eigenschaften sind Strings, und jeder Programmteil kann Zugriff auf beliebige Eigenschaften eines Objekts erhalten, indem er einfach nach dem Namen fragt. Sprachkonstrukte wie for...in-Schleifen und die ES5-Funktionen Object.keys() und Object.getOwnPropertyNames() machen es sogar ganz leicht, die Namen aller Eigenschaften eines Objekts in Erfahrung zu bringen.
Um Eigenschaften privat zu gestalten, greifen JavaScript-Programmierer oft auf allgemein anerkannte Schreibweisen zurück statt auf zuverlässige Mechanismen. Beispielsweise stellen viele dem Namen von privaten Eigenschaften einen Unterstrich voran (oder hängen ihn an). Dadurch werden in Wirklichkeit aber keinerlei Informationen verborgen, sondern nur wohlmeinende Benutzer eines Objekts darauf hingewiesen, dass sie die Eigenschaften nicht untersuchen oder ändern sollten, damit das Objekt die Freiheit behält, seine Implementierung zu ändern.
Wenn Sicherheit notwendig wird
Manche Programmierer brauchen aber mehr Sicherheit beim Verbergen von Informationen. Plattformen oder Anwendungsframeworks, bei denen es auf Sicherheit ankommt, müssen beispielsweise in der Lage sein, ein Objekt an eine nicht vertrauenswürdige Anwendung zu senden, ohne zu riskieren, dass sich jene Anwendung an den internen Mechanismen des Objekts zu schaffen macht. Eine andere Situation, in der Informationen zuverlässig verborgen werden müssen, liegt bei stark genutzten Bibliotheken vor, bei denen sich schwer zu diagnostizierende Fehler einschleichen können, wenn sich nachlässige Benutzer auf die Einzelheiten der Implementierung stützen oder daran herumpfuschen.
Closures
Für solche Situationen bietet JavaScript einen sehr zuverlässigen Mechanismus, um Informationen zu verbergen, nämlich Closures.
Closures sind sehr verschlossene Datenstrukturen. Sie speichern Daten in den enthaltenen Variablen, ohne einen direkten Zugriff darauf zu bieten. Die einzige Möglichkeit, um Zugang zum Inneren einer Closure zu bekommen, bietet eine Funktion, die diesen Zugriff ausdrücklich gewährt. Mit anderen Worten: Objekte und Closures verhalten sich auf diesem Gebiet genau entgegengesetzt: Die Eigenschaften von Objekten sind automatisch öffentlich zugänglich, die Variablen in einer Closure sind dagegen automatisch verborgen.
Private Daten speichern
Das können wir ausnutzen, um in einem Objekt Daten zu speichern, die dann wirklich privat sind. Anstatt diese Daten als Eigenschaften des Objekts abzulegen, halten wir sie als Variablen im Konstruktor fest und wandeln die Methoden des Objekts in Closures um, die auf diese Variablen verweisen. Unsere Klasse User aus Thema 30 sieht damit wie folgt aus:
function User(name, passwordHash) {
this.toString = function() {
return "[User " + name + "]";
};
this.checkPassword = function(password) {
return hash(password) === passwordHash;
};
}
Anders als in den früheren Implementierungen verweisen die Methoden toString und checkPassword jetzt als Variablen auf name und pass-wordHash und nicht als Eigenschaften von this. Eine Instanz von User enthält nun keinerlei Instanzeigenschaften mehr, weshalb externer Code keinen direkten Zugriff mehr auf ihren Namen und ihren Pass-wort-Hash hat.
Ein Nachteil dieser Methode besteht jedoch darin, dass die Methoden im Instanzobjekt platziert werden müssen, damit sich die Parameter des Konstruktors im Gültigkeitsbereich der Methoden befinden, von denen sie genutzt werden. Wie Sie in Thema 34 erfahren haben, kann das zu einer Vervielfältigung der Exemplare von Methoden führen. In Situationen, in denen das Verbergen von Informationen von entscheidender Bedeutung ist, können diese zusätzlichen Kosten aber gerechtfertigt sein.
Für eine korrekte Implementierung von Objekten ist es wichtig, sich genau über die 1:n-Beziehung zwischen einem Prototypobjekt und seinen Instanzen im Klaren zu sein. Eine mögliche Fehlerquelle besteht darin, versehentlich Instanzdaten im Prototyp zu speichern. Nehmen wir an, eine Klasse, die eine baumartige Datenstruktur implementiert, enthält ein Array der Kindobjekte für jeden Knoten. Wenn Sie dieses Array im Prototypobjekt platzieren, funktioniert die Implementierung nicht mehr:
function Tree(x) {
this.value = x;
}
Tree.prototype = {
children: [], // Dies sollte der Instanzstatus sein!
addChild: function(x) {
this.children.push(x);
}
};
Wenn wir nun versuchen, einen Baum mit dieser Klasse zu konstruieren, geschieht Folgendes:
var left = new Tree(2);
left.addChild(1);
left.addChild(3);
var right = new Tree(6);
right.addChild(5);
right.addChild(7);
var top = new Tree(4);
top.addChild(left);
top.addChild(right);
top.children; // [1, 3, 5, 7, left, right]
Bei jedem Aufruf von addChild hängen wir einen Wert an Tree.prototype.childen an, was aber die Knoten in der Reihenfolge aller Aufrufe von addChild von jeglicher Stelle enthält! Dadurch gelangen die Tree-Objekte in den inkohärenten Zustand aus Abbildung 4–5.
Abb. 4–5 Speicherung des Instanzstatus im Prototypobjekt
Um die Klasse Tree korrekt zu implementieren, müssen wir für jedes Instanzobjekt ein eigenes children-Array erstellen:
function Tree(x) {
this.value = x;
this.children = []; // Instanzstatus
}
Tree.prototype = {
addChild: function(x) {
this.children.push(x);
}
};
Wenn wir diesen Code ausführen, erhalten wir den gewünschten Status aus Abbildung 4–6.
Abb. 4–6 Speicherung des Instanzstatus in den Instanzobjekten
Daraus können wir lernen, dass Statusdaten bei der gemeinsamen Nutzung Probleme hervorrufen können. Methoden lassen sich im Allgemeinen gefahrlos von mehreren Instanzen einer Klasse nutzen, da sie gewöhnlich zustandslos sind und höchstens über this auf den Instanzstatus verweisen. (Da this aufgrund der Syntax von Methodenaufrufen selbst an das Instanzobjekt gebunden wird, wenn die Methode von einem Prototyp erbt, können gemeinsam genutzte Methoden nach wie vor auf den Instanzstatus zugreifen.) Auch jegliche unveränderliche Daten können im Allgemeinen gefahrlos über den Prototyp gemeinsam verwendet werden. Prinzipiell lassen sich statusbehaftete Daten ebenfalls im Prototyp speichern, sofern sie nicht für die gemeinsame Nutzung vorgesehen sind. Methoden sind jedoch die Daten, die am häufigsten in Prototypobjekten zu finden sind. Der Instanzstatus muss jedoch in den Instanzobjekten gespeichert werden.
Das Dateiformat CSV (Comma-Separated Values, kommagetrennte Werte) ist eine einfache Textdarstellung für tabellarische Werte:
Bösendorfer,1828,Wien,Österreich
Fazioli,1981,Sacile,Italien
Steinway,1853,New York,USA
Zum Lesen von CSV-Daten können wir eine einfache, anpassbare Klasse schreiben. (Der Einfachheit halber verzichten wir hier auf die Möglichkeit, Einträge in Anführungszeichen wie "hello, world" zu analysieren.) Die Bezeichnung CSV bedeutet zwar »kommagetrennt«, doch können bei diesem Format auch andere Trennzeichen verwendet werden. Unser Konstruktor nimmt daher ein optionales Array mit Trennzeichen entgegen und stellt dann einen maßgeschneiderten regulären Ausdruck zusammen, um jede Zeile in die einzelnen Einträge zu zerlegen:
function CSVReader(separators) {
this.separators = separators || [","];
this.regexp =
new RegExp(this.separators.map(function(sep) {
return "\\" + sep[0];
}).join("|"));
}
Eine einfache Implementierung einer Lesemethode (read) kann zwei Phasen umfassen: In der ersten wird der Eingabestring in ein Array aus einzelnen Zeilen aufgeteilt, und in der zweiten werden diese Zeilen jeweils in einzelne Zellen zerlegt. Das Ergebnis ist ein zweidimensionales Array aus Strings. Für diese Aufgabe ist die Methode map ideal geeignet:
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
return lines.map(function(line) {
return line.split(this.regexp);// Dies ist das falsche this!
});
};
var reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n"); // [["a,b,c"], ["d,e,f"]]
Dieser so einfach erscheinende Code weist einen schweren Fehler auf, der jedoch nicht leicht zu erkennen ist: Der an lines.map übergebene Callback verweist auf this und erwartet dabei, die Eigenschaft regexp des CSVReader-Objekts abzurufen. Tatsächlich aber bindet map den Empfänger seines Callbacks an das Array lines, das nicht über eine Eigenschaft dieses Namens verfügt. Das hat zur Folge, dass this.regexp ein undefiniertes Ergebnis liefert, sodass der Aufruf von line.split nicht funktioniert.
this ist an die nächste einschließende Funktion gebunden.
Der Fehler rührt daher, dass this auf eine andere Weise gebunden wird als Variablen. Wie in den Themen 18 und 25 erklärt, weist jede Funktion eine implizite Bindung von this auf, deren Wert beim Aufruf der Funktion bestimmt wird. Bei einer Variable mit lexikalischem Gültigkeitsbereich können Sie immer erkennen, woran sie gebunden ist, indem Sie nach einem expliziten Bindungsvorkommen des Namens suchen, beispielsweise in der Deklarationsliste von var oder in einem hinter function angegebenen Parameter. Dagegen ist this stets an die nächste einschließende Funktion gebunden. Daher weist this in CSVReader.prototype.read eine andere Bindung auf als in der Callback-Funktion, die an lines.map übergeben wird.
Zum Glück können wir ähnlich wie in dem forEach-Beispiel aus Thema 25 die Tatsache ausnutzen, dass die Arraymethode map ein optionales zweites Argument entgegennimmt, mit dem die Bindung von this für den Callback festgelegt wird. Die einfachste Korrekturmaßnahme in unserem Fall besteht also darin, die äußere Bindung von this mithilfe des zweiten Arguments von map an den Callback zu übertragen:
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
return lines.map(function(line) {
return line.split(this.regexp);
}, this); // Überträgt die äußere Bindung an den Callback
};
var reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");
// [["a","b","c"], ["d","e","f"]]
Allerdings sind nicht alle Callback-gestützten APIs so vorsorglich. Was wäre, wenn map dieses zusätzliche Argument nicht hätte? Dann müssten wir eine andere Möglichkeit finden, um in der Bindung von this den Zugriff auf die äußere Funktion zu erhalten und so dafür zu sorgen, dass der Callback nach wie vor darauf verweisen kann. Dazu gibt es eine recht unkomplizierte Lösung. Speichern Sie einfach einen zusätzlichen Verweis auf die äußere Bindung von this in einer Variable mit lexikalischem Gültigkeitsbereich:
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
var self = this; // Speichert einen Verweis auf
// die äußere this-Bindung
return lines.map(function(line) {
return line.split(self.regexp); // Verwendet das äußere this
});
};
var reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");
// [["a","b","c"], ["d","e","f"]]
self, me oder that
Für dieses Entwurfsmuster verwenden Programmierer gewöhnlich den Variablennamen self, um anzudeuten, dass der einzige Zweck dieser Variable darin besteht, als zusätzlicher Alias für die Bindung von this an den aktuellen Gültigkeitsbereich zu dienen. (Andere häufig vorkommende Namen für dieses Muster sind me und that.) Für das Funktionieren des Codes ist die Wahl des Namens zwar nicht wichtig, aber wenn Sie eine gebräuchliche Bezeichnung verwenden, helfen Sie anderen Programmierern, das Muster sofort zu erkennen.
Eine weitere Möglichkeit in ES5 besteht darin, die Methode bind der Callback-Funktion zu nutzen, wie wir es in ähnlicher Form schon in Thema 25 beschrieben haben:
CSVReader.prototype.read = function(str) {
var lines = str.trim().split(/\n/);
return lines.map(function(line) {
return line.split(this.regexp);
}.bind(this)); // Bindet an die äußere this-Bindung
};
var reader = new CSVReader();
reader.read("a,b,c\nd,e,f\n");
// [["a","b","c"], ["d","e","f"]]
Szenengraphen und Akteure
Ein Szenengraph (Scene Graph) ist eine Sammlung von Objekten, die eine Szene in einem grafischen Programm (wie einem Spiel oder einer Simulation) beschreibt. Eine einfache Szene enthält eine Sammlung aller Objekte, die sich in ihr befinden und die als Akteure bezeichnet werden, eine Tabelle der vorab geladenen Bilddaten dieser Akteure und einen Verweis auf die zugrunde liegende Grafikanzeige, die meistens als Kontext bezeichnet wird:
function Scene(context, width, height, images) {
this.context = context;
this.width = width;
this.height = height;
this.images = images;
this.actors = [];
}
Scene.prototype.register = function(actor) {
this.actors.push(actor);
};
Scene.prototype.unregister = function(actor) {
var i = this.actors.indexOf(actor);
if (i >= 0) {
this.actors.splice(i, 1);
}
};
Scene.prototype.draw = function() {
this.context.clearRect(0, 0, this.width, this.height);
for (var a = this.actors, i = 0, n = a.length;
i < n;
i++) {
a[i].draw();
}
};
Alle Akteure in einer Szene erben von der Grundklasse Actor, die die gemeinsamen Methoden abstrahiert. Jeder Akteur speichert einen Verweis auf seine Szene zusammen mit den Koordinaten seiner Position und fügt sich selbst zum Akteurregister der Szene hinzu:
function Actor(scene, x, y) {
this.scene = scene;
this.x = x;
this.y = y;
scene.register(this);
}
Um die Position eines Akteurs in der Szene zu ändern, stellen wir die Methode moveTo bereit, die die Koordinaten des Akteurs ändert und die Szene dann neu zeichnet:
Actor.prototype.moveTo = function(x, y) {
this.x = x;
this.y = y;
this.scene.draw();
};
Wenn ein Akteur die Szene verlässt, entfernen wir ihn aus dem Register der Szene und zeichnen sie neu:
Actor.prototype.exit = function() {
this.scene.unregister(this);
this.scene.draw();
};
Um einen Akteur zu zeichnen, schlagen wir das zugehörige Bild in der Grafiktabelle der Szene nach. Wir setzen hier voraus, dass jeder Akteur über das Feld type verfügt, über das wir das zugehörige Bild in der Tabelle suchen können. Sobald die Bilddaten vorliegen, können wir sie mithilfe der zugrunde liegenden Grafikbibliothek in den Grafikkontext zeichnen. (Bei diesem Beispiel verwenden wir die HTML-API Canvas, die die Methode drawImage zum Zeichnen eines Image-Objekts auf das <canvas>-Element einer Website bereitstellt.)
Actor.prototype.draw = function() {
var image = this.scene.images[this.type];
this.scene.context.drawImage(image, this.x, this.y);
};
Auf ähnliche Weise können wir die Größe eines Akteurs aus seinen Bilddaten ermitteln:
Actor.prototype.width = function() {
return this.scene.images[this.type].width;
};
Actor.prototype.height = function() {
return this.scene.images[this.type].height;
};
Besondere Typen von Akteuren implementieren wir als Subklassen von Actor. Für ein Raumschiff in einem Automatenspiel verwenden wir beispielsweise die Klasse SpaceShip, die Actor erweitert. Wie alle Klassen ist auch SpaceShip als Konstruktorfunktion definiert. Um sicherzustellen, dass die Instanzen von SpaceShip korrekt als Akteure initialisiert sind, muss ihr Konstruktur ausdrücklich den Konstruktor von Actor aufrufen. Dazu rufen wir Actor so auf, dass der Empfänger an das neue Objekt gebunden wird:
function SpaceShip(scene, x, y) {
Actor.call(this, scene, x, y);
this.points = 0;
}
Dadurch, dass wir als Erstes den Konstruktor Actor aufrufen, stellen wir sicher, dass dem Objekt alle von Actor erstellten Instanzeigenschaften hinzugefügt werden. Danach können wir die Instanzeigenschaften von SpaceShip selbst definieren hier beispielsweise points für den Punktestand des Schiffs.
Object.create
Damit SpaceShip eine richtige Subklasse von Actor wird, muss ihr Prototyp von Actor.prototype erben. Die beste Möglichkeit zu dieser Erweiterung bietet die ES5-Funktion Object.create:
SpaceShip.prototype = Object.create(Actor.prototype);
(In Thema 33 wurde eine Implementierung von Object.create für Umgebungen beschrieben, die ES5 nicht unterstützen.) Der Versuch, den Prototyp von SpaceShip mit dem Konstruktor Actor zu erstellen, würde verschiedene Probleme aufwerfen. Erstens haben wir keine sinnvollen Argumente, die wir Actor übergeben könnten:
SpaceShip.prototype = new Actor();
Den Superklassenkonstruktor nur vom Subklassenkonstruktor aus aufrufen
Wenn wir den Prototyp von SpaceShip initialisieren, haben wir noch keine Szenen erstellt, die wir als erstes Argument übergeben könnten. Außerdem verfügt der SpaceShip-Prototyp über keine sinnvollen x- und y-Koordinaten, denn sie sollten Instanzeigenschaften der einzelnen SpaceShip-Objekte sein, nicht von SpaceShip.prototype. Noch schwerer aber wiegt die Tatsache, dass der Konstruktor Actor das Objekt zum Register der Szene hinzufügt, was wir für den Prototyp von SpaceShip wirklich nicht wollen. Dies ist ein Phänomen, das häufig bei Subklassen auftritt: Der Superklassenkonstruktor sollte nur vom Subklassenkonstruktor aus aufgerufen werden, nicht beim Erstellen des Prototyps für die Subklasse.
Nachdem wir das Prototypobjekt von SpaceShip erstellt haben, können wir alle von sämtlichen Instanzen gemeinsam genutzten Eigenschaften hinzufügen. Dazu gehören unter anderem der Name type zur Indizierung der Bildtabelle für die Szene sowie die spezifischen Methoden von Raumschiffen.
SpaceShip.prototype.type = "spaceShip";
SpaceShip.prototype.scorePoint = function() {
this.points++;
};
SpaceShip.prototype.left = function() {
this.moveTo(Math.max(this.x - 10, 0), this.y);
};
SpaceShip.prototype.right = function() {
var maxWidth = this.scene.width - this.width();
this.moveTo(Math.min(this.x + 10, maxWidth), this.y);
};
Abbildung 4–7 zeigt ein Diagramm der Vererbungshierarchie für die Instanzen von SpaceShip. Wie Sie sehen, sind die Eigenschaften scene, x und y nur in den Instanzobjekten definiert, nicht aber in den Prototypobjekten, obwohl sie mit dem Konstruktor Actor erstellt wurden.
Abb. 4–7 Vererbungshierarchie mit Subklassen
Nehmen wir an, Sie wollen der Grafikbibliothek aus Thema 38 Möglichkeiten zum Erfassen von Diagnoseinformationen für das Debugging oder zum Erstellen eines Profils hinzufügen. Dazu wollen wir jeder Instanz von Actor eine eindeutige Identifizierungsnummer geben:
function Actor(scene, x, y) {
this.scene = scene;
this.x = x;
this.y = y;
this.id = ++Actor.nextID;
scene.register(this);
}
Actor.nextID = 0;
Nun wollen wir das Gleiche auch für die einzelnen Instanzen einer anderen Subklasse von Actor machen. Als Beispiel dafür nehmen wir die Klasse Alien für die Feinde unserer Raumschiffbesatzungen. Neben der allgemeinen Identifizierungsnummer für Akteure soll jeder Außerirdische auch eine eigene Identifizierungsnummer für Außerirdische haben.
function Alien(scene, x, y, direction, speed, strength) {
Actor.call(this, scene, x, y);
this.direction = direction;
this.speed = speed;
this.strength = strength;
this.damage = 0;
this.id = ++Alien.nextID; // Gerät in Konflikt mit der Akteur-ID!
}
Alien.nextID = 0;
Dieser Code führt zu einem Konflikt zwischen der Klasse Alien und ihrer Superklasse Actor, da beide versuchen, in die Instanzeigenschaft id zu schreiben. Jede dieser Klassen hält die Eigenschaft zwar für »privat« (also nur relevant und zugänglich für die direkt in der Klasse definierten Methoden), aber in Wirklichkeit ist diese Eigenschaft in den Instanzobjekten gespeichert und mit einem String benannt. Wenn zwei Klassen in einer Vererbungshierarchie auf denselben Eigenschaftsnamen verweisen, sprechen sie daher dieselbe Eigenschaft an.
Daher müssen sich Subklassen stets über alle Eigenschaften im Klaren sein, die ihre Superklassen nutzen, auch wenn diese Eigenschaften als privat konzipiert sind. Die auf der Hand liegende Lösung besteht in diesem Fall darin, unterschiedliche Eigenschaftsnamen für die Identifizierungsnummern von Actor und Alien zu verwenden:
function Actor(scene, x, y) {
this.scene = scene;
this.x = x;
this.y = y;
this.actorID = ++Actor.nextID; // Unterscheidet sich von alienID
scene.register(this);
}
Actor.nextID = 0;
function Alien(scene, x, y, direction, speed, strength) {
Actor.call(this, scene, x, y);
this.direction = direction;
this.speed = speed;
this.strength = strength;
this.damage = 0;
this.alienID = ++Alien.nextID; // Unterscheidet sich von actorID
}
Alien.nextID = 0;
Die ECMAScript-Standardbibliothek ist klein, enthält aber eine Handvoll wichtiger Klassen wie Array, Function und Date. Die Versuchung ist groß, sie durch Subklassen zu erweitern, doch leider umfassen ihre Implementierungen so viel besonderes Verhalten, dass es nicht möglich ist, saubere Subklassen zu schreiben.
Beispiel: Array
Ein gutes Beispiel bietet die Klasse Array. Wenn Sie eine Bibliothek für Operationen in einem Dateisystem schreiben, kann es sein, dass Sie eine Abstraktion der Verzeichnisse erstellen wollen, die das gesamte Verhalten von Arrays erbt:
function Dir(path, entries) {
this.path = path;
for (var i = 0, n = entries.length; i < n; i++) {
this[i] = entries[i];
}
}
Dir.prototype = Object.create(Array.prototype);
// Erweitert Array
Leider zeigt die Eigenschaft length von Arrays bei dieser Vorgehensweise nicht mehr das erwartete Verhalten:
var dir = new Dir("/tmp/mysite",
["index.html", "script.js", "style.css"]);
dir.length; // 0
[[Class]]
Der Grund dafür, dass dies nicht funktioniert, ist die Tatsache, dass die Eigenschaft length nur auf Objekten operiert, die intern als »echte« Arrays gekennzeichnet sind. Im ECMAScript-Standard ist dies als eine unsichtbare interne Eigenschaft namens [[Class]] angegeben. Lassen Sie sich von diesem Namen aber nicht täuschen, denn JavaScript hat kein geheimes internes Klassensystem. Der Wert von [[Class]] ist lediglich ein einfaches Kennzeichen. So werden Arrayobjekte (die mit dem Konstruktor Array oder mit [] erstellt wurden) mit dem [[Class]]-Wert "Array" versehen, Funktionen mit "Function" usw. Tabelle 4–1 zeigt sämtliche in ECMAScript definierten Werte für [[Class]].
Was aber hat diese magische Eigenschaft [[Class]] mit length zu tun? Für Objekte, deren interne Eigenschaft [[Class]] den Wert "Array" aufweist, ist ein besonderes Verhalten von length definiert: Bei solchen Objekten hält sich length über die Anzahl der indizierten Eigenschaften des Objekts auf dem Laufenden. Wenn Sie dem Objekt also weitere indizierte Eigenschaften hinzufügen, wächst length automatisch, und wenn Sie length verringern, werden automatisch alle indizierten Eigenschaften gelöscht, die über den neuen Wert hinausgehen.
Wenn wir nun aber die Klasse Array erweitern, werden die Instanzen der Subklasse nicht mehr mit new Array() oder der Literalsyntax [] erstellt, weshalb die Instanzen von Dir den [[Class]]-Wert "Object" aufweisen. Das können Sie sich sogar selbst ansehen, denn die Standardmethode Object.prototype.toString fragt die interne Eigenschaft [[Class]] des Empfängers ab, um eine allgemeine Beschreibung des Objekts zu erstellen. Wenn Sie diese Methode ausdrücklich für ein beliebiges Objekt aufrufen, können Sie den [[Class]]-Wert erkennen:
var dir = new Dir("/", []);
Object.prototype.toString.call(dir); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Daher erben Instanzen von Dir nicht das erwartete besondere Verhalten der Eigenschaft length für Arrays.
Tab. 4–1 Werte der internen Eigenschaft [[Class]] laut ECMAScript-Definition
[[Class]] |
Konstruktion |
"Array" |
new Array(...), [...] |
"Boolean" |
new Boolean(...) |
"Date" |
new Date(...) |
"Error" |
new Error(...), new EvalError(...), |
"Function" |
new Function(...), function(...) {...} |
"JSON" |
JSON |
"Math" |
Math |
"Number" |
new Number(...) |
"Object" |
new Object(...), {...}, new MyClass(...) |
"RegExp" |
new RegExp(...), /.../ |
"String" |
new String(...) |
Für eine bessere Implementierung von Dir müssen wir eine Instanzeigenschaft mit dem Array der Einträge definieren:
function Dir(path, entries) {
this.path = path;
this.entries = entries; // Arrayeigenschaft
}
Array-Methoden können Sie im Prototyp umdefinieren, indem Sie eine Delegierung zu den entsprechenden Methoden der Eigenschaft entries vornehmen:
Dir.prototype.forEach = function(f, thisArg) {
if (typeof thisArg === "undefined") {
thisArg = this;
}
this.entries.forEach(f, thisArg);
};
Array, Boolean, Date, Number ...
Bei den meisten Konstruktoren in der Standardbibliothek von ECMA-Script treten ähnliche Probleme auf, da manche Eigenschaften und Methoden einen bestimmten Wert von [[Class]] oder einer anderen internen Eigenschaft erwarten, den eine Subklasse nicht bieten kann. Aus diesem Grund sollten Sie von einer Vererbung der Standardklassen Array, Boolean, Date, Function, Number, RegExp und String absehen.
Objekte
Ein Objekt stellt seinen Nutzern eine kleine, einfache und leistungsfähige Menge von Operationen zur Verfügung. Die grundlegendsten Formen der Interaktion zwischen einem Nutzer und einem Objekt bestehen darin, dessen Eigenschaftswerte zu ermitteln und die Methoden aufzurufen. Bei diesen Operationen kommt es nicht darauf an, wo die Eigenschaften in der Prototyphierarchie gespeichert sind. Die Implementierung eines Objekts kann sich mit der Zeit weiterentwickeln, um eine Eigenschaft an verschiedenen Stellen seiner Prototypkette zu implementieren. Solange der Wert konsistent bleibt, laufen die Grundoperationen aber gleich ab. Einfach gesagt: Prototypen sind ein Implementierungsdetail des Objektverhaltens.
Introspection
Gleichzeitig bietet JavaScript auch einen bequemen Inspektionsmechanismus (Introspection) zur Untersuchung der Einzelheiten eines Objekts. Die Methode Object.prototype.hasOwnProperty ermittelt, ob eine Eigenschaft direkt als »eigene« Eigenschaft (also als Instanzeigenschaft) eines Objekts gespeichert ist, und ignoriert die gesamte Prototyphierarchie. Mit Object.getPrototypeOf und __proto__ (siehe Thema 30) können Programme die Prototypkette eines Objekts durchlaufen und sich die einzelnen Prototypobjekte ansehen. Das sind sehr leistungsfähige und manchmal auch sehr nützliche Möglichkeiten.
Gute Programmierer aber wissen, wann sie die Grenzen der Abstraktion berücksichtigen müssen. Wenn Sie die Einzelheiten der Implementierung abfragen, rufen Sie Abhängigkeiten zwischen den einzelnen Teilen eines Programms hervor – selbst dann, wenn Sie diese Details nicht bearbeiten. Wenn der Ersteller die Implementierung eines Objekts ändert, werden die Programmteile, die sich auf die Einzelheiten dieser Implementierung stützen, nicht mehr funktionieren. Solche Fehler lassen sich besonders schwer diagnostizieren, da Ursache und Wirkung an verschiedenen Stellen liegen: Ein Autor ändert die Implementierung einer Komponente, woraufhin eine andere Komponente (die meistens von einem ganz anderen Programmierer stammt) versagt.
»Private« Eigenschaften?
JavaScript unterscheidet auch nicht zwischen öffentlichen und privaten Eigenschaften eines Objekts (siehe Thema 35). Dies liegt in Ihrer Verantwortung, wobei Sie die Dokumentation zurate ziehen und diszipliniert vorgehen müssen. Wenn eine Bibliothek ein Objekt mit nicht dokumentierten oder eigens als intern bezeichneten Eigenschaften bereitstellt, sollten sich die Nutzer tunlichst nicht an diesen Eigenschaften zu schaffen machen.
Nachdem ich mich in Thema 41 vehement gegen Verletzungen der Abstraktion ausgesprochen habe, wollen wir uns nun die größtmögliche Form einer solchen Verletzung ansehen. Da Prototypen als Objekte gemeinsam verwendet werden, kann jeder Eigenschaften hinzufügen, entfernen und verändern. Diese umstrittene Praxis wird gewöhnlich als Monkey-Patching bezeichnet.
Was Monkey-Patching ansprechend macht, ist die große Band-breite an Möglichkeiten, die es bietet. Sie vermissen eine nützliche Methode für Arrays? Kein Problem, fügen Sie sie einfach selbst hinzu:
Array.prototype.split = function(i) { // Alternative 1
return [this.slice(0, i), this.slice(i)];
};
Voilà: Schon verfügen alle Arrayinstanzen über die Methode split.
Probleme treten jedoch auf, wenn mehrere Bibliotheken auf diese Weise unvereinbare Änderungen an demselben Prototyp vornehmen. Stellen Sie sich vor, eine weitere Bibliothek versieht Array.prototype mit einer anderen Methode desselben Namens:
Array.prototype.split = function() { // Alternative 2
var i = Math.floor(this.length / 2);
return [this.slice(0, i), this.slice(i)];
};
Bei jeder Verwendung von split für ein Array besteht eine Chance von 50 %, dass sie nicht funktioniert – je nachdem, welche der beiden Methoden erwartet wird.
Jede Bibliothek, die gemeinsam verwendete Prototypen (wie Array. prototype) bearbeitet, sollte dies zumindest deutlich dokumentieren. Dadurch bekommen Clients wenigstens eine Warnung vor möglichen Konflikten zwischen Bibliotheken. Das ändert aber nichts an der Tatsache, dass zwei Bibliotheken, die denselben Prototyp auf gegensätzliche Weise verändern, nicht innerhalb desselben Programms verwendet werden können. Eine Möglichkeit besteht darin, dass eine Bibliothek solche Monkey-Patches nur als Option anbietet und die Änderungen in einer Funktion bereitstellt, die die Benutzer nach Belieben aufrufen oder ignorieren können:
function addArrayMethods() {
Array.prototype.split = function(i) {
return [this.slice(0, i), this.slice(i)];
};
};
Diese Vorgehensweise funktioniert natürlich nur dann, wenn die Bibliothek, die addArrayMethods bereitstellt, nicht vom Vorhandensein von Array.prototype.split abhängt.
Polyfills
Trotz dieser Gefahren gibt es jedoch eine zuverlässig funktionierende und auch unschätzbar wertvolle Anwendung für das Monkey-Patching, nämlich Polyfills (nach einem britischen Markennamen für Spachtelmasse). JavaScript-Programme und -Bibliotheken werden oft auf mehreren Plattformen bereitgestellt, z.B. auf Webbrowsern verschiedener Hersteller und verschiedener Versionen. Je nach Plattform kann es Abweichungen in der Anzahl der implementierten Standard-APIs geben. Beispielsweise definiert ES5 die neuen Array-Methoden forEach, map und filter, die jedoch von einigen Browserversionen nicht unterstützt werden. Allerdings sind viele Programme und Bibliotheken von ihnen abhängig. Da das Verhalten dieser fehlenden Methoden durch einen weithin unterstützten Standard definiert ist, können Sie eine Implementierung dafür bereitstellen, ohne Gefahr zu laufen, eine Inkompatibilität der Bibliotheken heraufzubeschwören. Es ist kein Problem, wenn mehrere Bibliotheken Implementierungen derselben Standardmethoden bereitstellen (sofern diese Implementierungen korrekt gestaltet sind), da sie alle dieselbe Standard-API implementieren.
Solche Lücken in den Plattformen können Sie gefahrlos »zuspachteln«, indem Sie die Monkey-Patches mit einem Test absichern:
if (typeof Array.prototype.map !== "function") {
Array.prototype.map = function(f, thisArg) {
var result = [];
for (var i = 0, n = this.length; i < n; i++) {
result[i] = f.call(thisArg, this[i], i);
}
return result;
};
}
Da zunächst auf das Vorhandensein von Array.prototyp.map geprüft wird, ist sichergestellt, dass eine mitgelieferte (und damit wahrscheinlich effizientere und besser getestete) Implementierung nicht überschrieben wird.