- Einleitung ↓
- Methodenverkettung (Method Chaining) ↓
- Fluent Interfaces und Domain-Specific Languages ↓
- Die Möglichkeiten von ECMAScript 5 ↓
- Massenhaft Eigenschaften zuweisen ↓
- ECMAScript 5 Property Descriptors 1 – Werte und Zugriffsrechte ↓
- ECMAScript 5 Property Descriptors 2 – Getter und Setter ↓
- Chaining durch ein Proxy-Objekt ermöglichen ↓
- Proxy-Objekte in ECMAScript Harmony ↓
- Allgemeiner Chaining-Helfer für Eigenschaften und Methoden ↓
- Praxisanwendung ↓
- Links und Quellen ↓
- Danksagung ↓
Einleitung
Der folgende Beitrag bietet zunächst einen Überblick über ein verbreitetes Muster von JavaScript-Programmierschnittstellen (APIs). Im Anschluss daran werden API-Ideen vorgestellt, die auf standardisierten ECMAScript-5- und zukünftigen ECMAScript-Harmony-Techniken basieren. Die neuen ECMAScript-Möglichkeiten eignen sich nämlich hervorragend, um APIs zu entwerfen, mit denen sich Programmlogiken komfortabel und lesbar in Code ausdrücken lassen. Der Beitrag soll daher auch Einblick in einige wichtige ECMAScript-Neuerungen bieten.
Methodenverkettung (Method Chaining)
Zunächst, was ist Methodenverkettung? Es kommt häufig vor, dass mehrere Methoden eines Objekts hintereinander aufgerufen werden. Das sieht etwa so aus:
var objekt = new BeispielObjekt(); objekt.methode1(); objekt.methode2(); var weiteresObjekt = objekt.methode3(); weiteresObjekt.methode1(); weiteresObjekt.methode2();
Der Nachteil ist hier, dass jeder Methodenaufruf ein einzelnes Statement sein muss. In jeder Zeile wird der Objektname wiederholt, obwohl nur der Methodenaufruf wichtig ist. Die Kerninformation, der Methodenaufruf, droht unterzugehen. Die »Signal-Rausch-Verhältnis« eines solchen Codes ist daher schlecht, er ist aufgebläht und redundant.
Methodenverkettung ändert nun die Signatur von Methoden – in erster Linie derjenigen Methoden, die keinen Rückgabewert (also undefined
) besitzen. Diese Methoden werden so geändert, dass sie das Objekt zurückgeben, auf dem sie aufgerufen werden.
Die API von jQuery verwendet Methodenverkettung ziemlich konsequent und ist daher ein gutes Beispiel. jQuery-Objekte sind Listen von DOM-Elementobjekten. Auf diesem Wrapper-Objekt lassen sich gewisse Methoden aufrufen. Alle Methoden, die keinen natürlichen Rückgabewert haben, geben wieder das Wrapper-Objekt zurück. Dadurch kann man folgende Methodenketten schreiben:
$('#ausgabe').find('h2').html('Wichtige Nachricht!').end().fadeIn('fast').delay(5000).slideUp();
Das Beispiel spricht ein Element mit der ID ausgabe
an, sucht darin ein h2
-Element und ändert dessen Inhalt. Anschließend blendet es das gesamte Element ein und nach fünf Sekunden wieder aus.
Innerhalb einer Zeile wird eine komplexe Logik sehr konzise ausgedrückt. jQuerys Motto lautet entsprechend »write less, do more«. Wenn diese dichte Kompaktschreibweise stört, können die einzelnen Methodenaufruf der Übersicht halber auf mehrere Zeilen verteilt und eingerückt werden:
$('#ausgabe') .find('h2') .html('Wichtige Nachricht!') .end() .fadeIn('fast') .delay(5000) .slideUp();
Nun sind jQuery-Scripte nicht eine endlose Kette von Methodenaufrufen. Die Ketten brechen üblicherweise nach ein paar Methodenaufrufen ab, denn nicht alle Methoden erlauben eine Verkettung. Viele Methoden sind Getter, geben bereits einen bestimmten Primitive-Wert (String
, Number
, Boolean
) oder ein ganz anderes Objekt zurück. In diesen Fällen ist Chaining nicht verwendbar:
var input = $('input[name=email]'); var email = input.val(); if (jQuery.trim(email) === '') { input.addClass('error'); }
Andere Methoden arbeiten asynchron mit Callbacks, sodass erst im Callback weitergearbeitet werden kann:
$('#ausgabe').load('/nachricht', function () { $(this).fadeIn(); });
Für Animationen besitzt jQuery eine interne Warteschlange (Queue), sodass sich die Effekte verketten lassen, aber erst nacheinander ausgeführt werden. Das erste Beispiel hat dies gezeigt:
.fadeIn('fast').delay(5000).slideUp()
jQuery bietet seit Version 1.5 auch eine Möglichkeit an, um asynchrone Operationen wie Ajax zu verknüpfen. Die Technik dazu lautet Deferreds bzw. Promises.
$.when($.get('/eins'), $.get('/zwei')) .success(function(args1, args2) { alert('Eins: ' + args1[0] + '\nZwei: ' + args2[0]); });
jQuery geht zwar schon ziemlich weit, aber besonders bei asynchronen Operationen ließe sich das Method Chaining noch weiter treiben. Eine fiktive API, welche noch stärker auf Promises setzt, könnte das obige load
-Beispiel so umsetzen:
// Fiktiver Code, kein lauffähiges jQuery 1.5 $('#ausgabe').load('/nachricht').then().fadeIn();
Durch den Aufruf von then
würden die darauffolgenden Methodenaufrufe verzögert.
Fluent Interfaces und Domain-Specific Languages
Der Begriff Fluent Interface bezeichnet APIs, deren Code sich »flüssig« wie Sätze einer natürlichen Sprache lesen lässt. Methodenverkettung ist eine Komponente, um solche Sätze zu bilden. Eine Fortentwicklung dieser Idee sind APIs, welche auf Basis einer Programmiersprache eine andere, oft deklarative Sprache erfinden, die einen ganz bestimmten Zweck erfüllt. Solche Sprachen werden Domain-specific Languages (DSL) genannt, Sprachen für bestimmte Aufgabenbiete.
Rund um die Programmiersprache Ruby sowie Ruby on Rails, ein bekanntes Framework für Webanwendungen, haben sich verschiedene DSLs entwickelt. Es fängt in Rails mit einfachen Ausdrücken an:
> 5.days.ago => Tue, 08 Mar 2011 23:20:04 UTC +00:00
Arel, eine in Rails integrierte DSL für Datenbankabfragen, ist ein weiteres Beispiel:
users.where( users[:name].eq('bob').or( users[:age].lt(25) ))
Schließlich RSpec, ein Testing-Framework in Ruby:
describe Account do context "transfering money" do it "reduces its balance by the transfer amount" do source = Account.new(50, :USD) target = stub('target account') source.transfer(5, :USD).to(target) source.balance.should == Money.new(45, :USD) end end end
Dieser Test lässt sich für einigermaßen Programmiererfahrene lesen und verstehen, ohne Ruby oder den zu testenden Code zu kennen. Auch wenn es so aussieht, als wäre dies eine eigene Sprache, so sind es lediglich Methoden der Rails- und RSpec-Bibliothek, die aufgerufen werden und denen Codeblöcke übergeben werden. Die flexible Ruby-Syntax und seine Metaprogramming-Fähigkeiten machen es möglich, solche DSLs mit sprechendem Code zu entwerfen.
Die Möglichkeiten von ECMAScript 5
Doch nun wieder zurück zu JavaScript. Im Folgenden möchte ich verschiedene Ideen wiedergeben und ausarbeiten, die Tim Tepaße in einem Posting im SELFHTML-Forum geäußert hat. Er macht sich darin Gedanken, wie sich Interfaces wie die obigen in JavaScript umsetzen lassen. Dabei gibt er verschiedene Beispiele, die bereits ECMAScript-5-Features nutzen.
Massenhaft Eigenschaften zuweisen
Es fängt mit der Aufgabe an, einem Objekt nach seiner Erzeugung nacheinander mehrere Eigenschaften zuzuweisen. Ausgangspunkt ist folgende Struktur:
objekt.eigenschaft1 = 'wert1'; objekt.eigenschaft2 = 'wert2'; objekt.eigenschaft3 = 'wert3'; objekt.eigenschaft4 = 'wert4';
Diese Schreibweise ist sehr redundant. JavaScript besitzt das with
-Statement, mit dem sich solcher Code kürzer schreiben lässt:
with (objekt) { eigenschaft1 = 'wert1'; eigenschaft2 = 'wert2'; eigenschaft3 = 'wert3'; eigenschaft4 = 'wert4'; }
Der obige konkrete Anwendungsfall von with
ist unproblematisch, denn es werden nur feste Strings zugewiesen. In vielen anderen Anwendungsfällen, in denen weitere Bezeichner und Zuweisungen ins Spiel kommen, kann with
jedoch kann zu langsamem, mehrdeutigem und unzuverlässigem Code führen. Daher wird von dessen Gebrauch generell abgeraten. Im Strict-Mode von ECMAScript 5, in dem zahlreiche problematische Features deaktiviert sind, ist with
daher gar nicht mehr verwendbar.
Wenn möglich, werden die Eigenschaften bereits bei der Erzeugung des Objekts gesetzt, anstatt die Eigenschaften einzeln zu setzen. Dazu dient ein Object-Literal:
var objekt = { eigenschaft1: 'wert1', eigenschaft2: 'wert2', eigenschaft3: 'wert3', eigenschaft4: 'wert4' };
Hier soll es allerdings um die Fälle gehen, die dadurch nicht abgedeckt sind. Tims Forumsbeitrag gibt ein Beispiel, in dem einer Konstruktorfunktion ein Object
-Literal mit Optionen übergeben wird. Der Konstruktor kann das Objekt durchlaufen und dessen Eigenschaften an der neuen Instanz speichern. Solche Optionen-Hashes sind auch bei Ruby on Rails gängig.
function Baby (options) { for (var propName in options) { if (options.hasOwnProperty(propName)) { this[propName] = options[propName]; } } } var ada = new Baby({ born: new Date(), name: 'Ada', height: 0.55, weight: 3.5, gender: 'f', hair: null, eyes: { left: 'blue', right: 'brown' } });
ECMAScript 5 Property Descriptors 1 – Werte und Zugriffsrechte
Im obigen Beispiel übernimmt der Baby
-Konstruktor die Aufgabe, die Eigenschaften des übergebenen Objects an die Instanz zu kopieren. Wenn man mehrere derartige Konstruktoren hat, liegt es nahe, diese Aufgabe einmal zentral zu lösen. Tim Tepaße hat dazu die Idee einer Helfermethode setProperties
. Diese Methode wird mit folgendem ECMAScript-5-Code beim Prototyp der Baby
-Instanzen (Baby.prototype
) erzeugt:
Object.defineProperty(Baby.prototype, 'setProperties', { value: function (props) { var self = this; Object .keys(props) .filter(props.hasOwnProperty, props) .forEach(function (name) { self[name] = props[name]; }); }, writable: true, enumerable: false, configurable: true });
Bei der Anwendung der Methode wird ihr das bekannte Object-Literal mit Eigenschaften übergeben, die ans Objekt kopiert werden:
var ada = new Baby(); ada.setProperties({ born: new Date(), name: 'Ada', height: 0.55, weight: 3.5, gender: 'f', hair: null, eyes: { left: 'blue', right: 'brown' } });
Doch nun zur Erklärung des Codes, der die Methode definiert. Sie besteht zunächst aus einem Aufruf der ECMAScript-5-Methode Object.defineProperty. Damit lässt sich eine Eigenschaft bzw. Methode bei einem angegebenen Objekt erzeugen. Das Aufrufschema lautet:
Object.defineProperty(objekt, 'eigenschaftsname', descriptorObjekt);
Der Unterschied zur einfachen und bekannten Schreibweise objekt.eigenschaft = wert
ist, das sich ein Property Descriptor (»Eigenschafts-Beschreiber«) angeben lässt. Ein Property Descriptor ist wiederum ein einfaches Objekt, welches im Beispiel mit einem Object-Literal notiert wird. Darin werden neben dem Eigenschaftswert gewisse Zugriffsrechte deklariert.
Der Wert der zu erzeugenden Eigenschaft wird in der value
-Eigenschaft des Descriptors angegeben. Im Beispiel handelt es sich um eine Funktion. Zusätzlich werden drei weitere Boolean-Eigenschaften mit den Zugriffsrechten gesetzt:
writable
- Kann der Wert der Eigenschaft durch eine Zuweisung nachträglich geändert werden?
Im Beispiel: ja configurable
- Können die Zugriffsrechte der Eigenschaft (der Descriptor) nachträglich geändert werden und kann die Eigenschaft per
delete
gelöscht werden?
Im Beispiel: ja enumerable
- Taucht die Eigenschaft beim Durchlaufen aller Eigenschaften mit einer
for-in
-Schleife auf?
Im Beispiel: nein
Die ersten beiden Zugriffsrechte bleiben also gewahrt. setProperties
kann später wieder überschrieben oder gelöscht werden. Sie taucht allerdings nicht in for-in
-Schleifen auf.
Nach dem defineProperty
-Aufruf steht eine Methode setProperties
durch prototypische Vererbung bei jeder Baby
-Instanz zur Verfügung. Was passiert nun in dieser Methode? Es werden weitere ECMAScript-5-Methoden verwendet, um die Eigenschaften des übergebenen Objekts an das gegenwärtige Objekt zu kopieren. ECMAScript 5 vereinfacht das Arbeiten mit Listen durch verschiedene neue Methoden, die an Ruby und Python erinnern:
Object // Hole eine Liste aller Eigenschaften des Objects: .keys(props) // Filtere diese Liste mithilfe von hasOwnProperty, // sodass vererbte Eigenschaften ignoriert werden: .filter(props.hasOwnProperty, props) // Führe für jede Eigenschaft eine Funktion aus .forEach(function (name) { // Kopiere die Eigenschaft vom übergebenen Objekt zum aktuellen Objekt self[name] = props[name]; });
Diese Aufgaben lassen sich natürlich auch in ECMAScript-3-Syntax imperativ umsetzen, doch diese Schreibweise macht uns mit neuen funktionalen ECMAScript-5-Methoden vertraut.
ECMAScript 5 Property Descriptors 2 – Getter und Setter
ECMAScript 5 standardisiert eine verbreitete Technik der Objektorientierung: Getter- und Setter-Methoden. Kurz gesagt sind das Eigenschaften, bei deren Auslesen und Setzen eine angegebene Funktion ausgeführt wird. Während wir uns bisher mit objekt.getEigenschaft()
und objekt.setEigenschaft(5)
behelfen mussten, können wir mit ECMAScript 5 nun einfach objekt.eigenschaft
und objekt.eigenschaft = 5
schreiben. Hinter den Kulissen werden automatisch die gewünschten Getter- und Setter-Methoden aufgerufen.
Schon vor ECMAScript 5 gab es Getter und Setter in JavaScript mithilfe der Methoden __defineGetter__
und __defineSetter__
. Diese sind recht breit unterstützt, sind aber nicht standardisiert. ECMAScript 5 integriert Getter und Setter kurzerhand in sein Konzept der Property Descriptors. Anstelle der value
-Eigenschaft kann der Descriptor die Eigenschaften get
und/oder set
besitzen:
get
- Gibt eine Funktion an, welche beim Auslesen des Eigenschaftswertes ausgeführt wird. Sie sollte mit
return
einen Wert zurückgeben. set
- Gibt eine Funktion an, welche beim Setzen des Eigenschaftswertes ausgeführt wird. Sie erhält den neuen Wert als ersten Parameter.
Mit diesem Wissen lässt sich das obige setProperties
-Beispiel mit einem nativen Setter namens properties
umsetzen:
Object.defineProperty(Baby.prototype, 'properties', { set: function (props) { var self = this; Object .keys(props) .filter(props.hasOwnProperty, props) .forEach(function (name) { self[name] = props[name]; }); }, enumerable: false, configurable: true });
Anstelle einer setProperties
-Methode wird nun ein properties
-Setter definiert. Wird dieser Eigenschaft ein Wert zugewiesen, wird die angegebene Funktion ausgeführt. Das Baby-Beispiel kann damit wie folgt umgeschrieben werden:
var ada = new Baby(); ada.properties = { born: new Date(), name: 'Ada', height: 0.55, weight: 3.5, gender: 'f', hair: null, eyes: { left: 'blue', right: 'brown' } };
Chaining durch ein Proxy-Objekt ermöglichen
Nachdem wir das massenhafte Zuweisen von Eigenschaften betrachtet haben, wenden wir uns wieder der Verkettung von Methoden zu. Auch diese lässt sich mit ECMAScript 5 vereinfachen. Damit Methodenverkettung möglich ist, muss wie gesagt jede beteiligte Methode entsprechend vorbereitet sein und wieder das Objekt zurückgeben. Nicht alle APIs sind entsprechend ausgelegt und nicht alle Methoden können das Objekt zurückgeben.
Das bedeutet nicht, dass man auf Chaining verzichten muss. Es ist möglich, eine API nachträglich Chaining-fähig zu machen, ohne den Ursprungscode zu ändern. Tim Tepaße schlägt dafür ein Proxy-Objekt vor, welches die Methodenaufrufe kapselt. Anstelle der Original-Funktion wird eine Wrapper-Funktion des Proxy-Objekts aufgerufen. Diese ruft die Original-Funktion auf und gibt in jedem Fall wieder das Objekt zurück, sodass Chaining möglich ist.
Object.defineProperty(Baby.prototype, 'chaining', { get: function () { var original = this, toString = Object.prototype.toString, proxy; // Falls bereits ein Proxy-Objekt existiert, gib es zurück if (original.hasOwnProperty('_proxy')) { return original._proxy; } // Erzeuge das Proxy-Objekt proxy = {}; // Durchlaufe alle Eigenschaften des Originals (auch vererbte) for (var propName in original) { var propVal = original[propName]; // Behandle nur Methoden, überspringe andere Eigenschaften if (typeof propVal != 'function' && toString.call(propVal) != '[object Function]') { continue; } // Sofort ausgeführter Funktionsausdruck als Closure (function closure (propName) { // Lege für jede gefundene Methode eine gleichnamige Methode // beim Proxy-Objekt an proxy[propName] = function chainableMethodWrapper () { // Darin wird die Original-Methode aufgerufen original[propName].apply(original, arguments); // Und das Proxy-Objekt zurückgegeben return proxy; }; })(propName); } propVal = null; // Speichere das Proxy-Objekt in der Eigenschaft _proxy beim Originalobjekt Object.defineProperty(original, '_proxy', { value: proxy, // Die _proxy-Eigenschaft ist löschbar/konfigurierbar, // aber nicht überschreibbar und zählbar writable: false, enumerable: false, configurable: true }); return proxy; }, // Der chaining-Getter ist löschbar/konfigurierbar, aber nicht zählbar enumerable: false, configurable: true });
Der obige Code definiert eine Getter-Eigenschaft namens chaining
beim Baby
-Prototyp. Dieses Schema ist bereits aus dem vorherigen Beispiel bekannt. Innerhalb der Getter-Funktion wird ein Proxy-Objekt erzeugt. Bei diesem wird für jede Methode des Original-Objekts eine Chaining-fähige Wrapper-Methode angelegt. Das Proxy-Objekt wird schließlich in einer Eigenschaft _proxy
zwischengespeichert, damit es pro Objekt nur einmal angelegt werden muss.
Mithilfe des nun verfügbaren chaining
-Getters können wir Methodenverkettung bei jedem denkbaren Objekt anwenden. Der Clou dabei ist, dass das Original-Objekt (bis auf die interne _proxy
-Eigenschaft) nicht angetastet wird. Dafür müssen wir das Chaining mit einem Aufruf des chaining
-Getters einleiten, welcher das Proxy-Objekt zurückgibt.
Als Anwendungsbeispiel dient wieder Ada Lovelace. Gehen wir davon aus, dass ein Haufen von überladenen Methoden existiert (born
, name
, height
usw.), die im Stile von jQuery das Setzen und Auslesen von Eigenschaften erlauben. Diese werden an den Prototyp geheftet:
function Baby () { this.properties = {}; } Baby.prototype = { born: function (v) { if (arguments.length) { this.properties.born = v; } else { return this.properties.born; } }, // … viele weitere nach demselben Schema … };
Selbst wenn diese Methoden von sich aus kein Chaining erlauben, so ist mit unserem Helfer nun folgendes möglich:
var ada = new Baby(); ada.chaining .born(new Date()) .name('Ada') .height(0.55) .weight(3.5) .gender('f') .hair('none') .eyes({ left: 'brown', right: 'blue' });
Auch hier gilt: Das Konzept eines Proxy-Objekts erfordert nicht zwingend ECMAScript-5-Getter und könnte auch mit einer herkömmlichen Methode in ECMAScript 3 umgesetzt werden. ECMAScript 5 macht die Anwendung lediglich komfortabler.
Proxy-Objekte in ECMAScript Harmony
Das Proxy-Pattern stellt eine Möglichkeit dar, JavaScript-Interfaces expressiver zu gestalten. Um mit JavaScript bisher unmögliche oder untypische Interfaces sowie relativ freie DSLs zu ermöglichen, wird die kommende ECMAScript-Version mit dem Codenamen Harmony native Proxies unterstützen.
Ein solches Proxy-Objekt fängt sämtliche Operationen ab und führt entsprechende Proxy-Funktionen aus. Diese können dann entscheiden, was sie an das Original-Objekt durchgeben. Damit lässt das Verhalten der Objekt-Operationen komplett umdefinieren. Man spricht daher auch von Meta-Programming (Metaprogrammierung), da der letztlich ausgeführte Code zur Laufzeit generiert wird. So lässt sich etwa method_missing
aus Ruby implementieren – eine Methode, die automatisch Anfragen an nicht definierte Eigenschaften abfängt und gegebenenfalls on-the-fly eine passende Methode erzeugt oder an eine andere delegiert.
Das oben gezeigte Chaining mithilfe eines einmal angelegten und anschließend gecachten Proxy-Objekts trägt nicht der Dynamik von JavaScript Rechnung, wie Tim Tepaße auch in seinem Forumsbeitrag anmerkt. Kommen neue Methoden hinzu, so bietet das Proxy-Objekt nicht automatisch passende Wrapper.
Mit ECMAScript Harmony wird sich dieses Problem lösen lassen. Auch wenn sich ECMAScript Harmony im Gegensatz zum Ende 2009 verabschiedeten ECMAScript 5 noch in der Entwicklung befindet, so bietet Firefox 4 bereits eine experimentelle Umsetzung von Harmony-Proxies.
Das Kernstück des chaining
-Beispiels lässt sich durch ein natives Proxy
-Objekt austauschen, welches sämtliche Eigenschaftsanfragen an das Original delegiert.
Object.defineProperty(Baby.prototype, 'chaining', { get: function () { var original = this, toString = Object.prototype.toString, proxy; if (original.hasOwnProperty('_proxy')) { return original._proxy; } // Erzeuge ein natives Proxy-Objekt (ECMAScript Harmony) proxy = Proxy.create({ get: function (receiver, propName) { var propVal = original[propName]; // Handelt es sich um eine Funktion? if (typeof propVal === 'function' || toString.call(propVal) === '[object Function]') { propVal = null; // Gib eine Chaining-fähige Wrapper-Funktion zurück return function chainableMethodWrapper () { original[propName].apply(original, arguments); return proxy; }; } // Andernfalls gib den Eigenschaftswert zurück return propVal; } }); Object.defineProperty(this, '_proxy', { value: proxy, writable: false, enumerable: false, configurable: true }); return proxy; }, enumerable: false, configurable: true });
Wenn das Proxy-Objekt eine Anfrage an eine Methode registriert, dann gibt es automatisch eine Chaining-fähige Wrapper-Funktion zurück. Andernfalls gibt es die Original-Eigenschaft zurück. Diese kann selbstverständlich undefiniert (undefined
) sein.
Das Proxy-Objekt im Beispiel hat bewusst eine sehr beschränkte Funktionalität, denn es implementiert nur das Abfragen von Eigenschaften – mehr ist für Methodenverkettung nicht nötig. Sämtliche anderen Objekt-Operationen, die mit dem Beispiel-Proxy vorgenommen werden, führen zu einer Exception (einem Laufzeitfehler), da für diese Fälle (Traps genannt) keine Handler-Funktionen definiert sind.
Allgemeiner Chaining-Helfer für Eigenschaften und Methoden
Die bisherigen Beispiele vereinfachen entweder das Setzen von Eigenschaften oder das Chaining von Methoden. Bei jQuery funktioniert das Auslesen und auch das Setzen über dieselbe, überladene Methode. Beispielsweise liest .css('font-size')
den gegenwärtigen font-size-Wert aus, während .css('font-size', '15px')
ihn setzt.
Das Baby.prototype.chaining
-Beispiel ging davon aus, dass solche überladenen Methoden bereits existieren. Tim Tepaße schlägt vor, beim Proxy-Objekt das Aufrufen von Methoden sowie das Auslesen und Setzen von Eigenschaften zu ermöglichen. Beim Original-Objekt müssen also nicht notwendig überladene Methoden existieren, sie werden automatisch vom Proxy erzeugt.
Um das Proxy-Objekt zu erzeugen, könnte man eine universell einsetzbare Helferfunktion verwenden, beispielsweise using()
. Sie nimmt das Original-Objekt entgegen und gibt ein Chaining-fähiges Proxy-Objekt zurück. Das vereinfachte Erzeugen des Baby
-Objekts sieht damit so aus:
var ada = new Baby(); using(ada) // Eigenschaften setzen .born(new Date()) .name('Ada') .height(0.55) .weight(3.5) .gender('f') .hair('none') .eyes({ left: 'brown', right: 'blue' }) // Vorhandene Methoden aufrufen .cry() .crawl(); // Eigenschaft über den Proxy auslesen alert( using(ada).name() ); // ergibt Ada // Zur Kontrolle: Eigenschaft des Original-Objekts auslesen alert( ada.name ); // ergibt Ada
Der Quellcode der using
-Funktion könnte so aussehen:
function using (original) { if (!window.Proxy) { throw new Error('using(): ECMAScript Harmony Proxies not available'); } var toString = Object.prototype.toString; var proxy = Proxy.create({ get: function (receiver, propName) { var propVal = original[propName]; // Handelt es sich um eine Funktion? if (typeof propVal === 'function' || toString.call(propVal) === '[object Function]') { propVal = null; // Gebe eine Chaining-fähige Wrapper-Funktion zurück return function chainableMethodWrapper () { original[propName].apply(original, arguments); return proxy; }; } else { propVal = null; // Gebe eine Chaining-fähige, überladene Getter-/Setter-Methode zurück return function chainablePropertyWrapper (val) { if (arguments.length) { original[propName] = val; return proxy; } else { return original[propName]; } }; } } }); return proxy; }
using
erzeugt ebenfalls einen ECMAScript-Harmony-Proxy. In dem Handler für die Eigenschaftsabfrage wird zunächst geprüft, ob eine Methode existiert, die den gewünschten Namen besitzt. Wenn dies zutrifft, wird der bekannte Methoden-Wrapper zurückgegeben. Im anderen Fall wird eine überladene Methode erzeugt, mit dem sich die Eigenschaft mit dem gewünschten Namen lesen oder setzen lässt.
In diesem Beispiel wird auf das Caching des Proxy-Objekts und auf das Caching der erzeugten Funktionen verzichtet. Dies kann natürlich außerhalb von using
erfolgen, indem dessen Rückgabewert, das Proxy-Objekt, in einer Variable gespeichert wird.
Praxisanwendung
Die obigen Beispiele sind etwas gestellt, da sie lediglich die Möglichkeiten von Interfaces mit ECMAScript 5 und ECMAScript Harmony demonstrieren sollen. Es gibt aber bereits sehr gute Beispiele für Fluent Interfaces, die mit den neuen Techniken gebaut wurden.
Ein Beispiel ist should.js. Dies ist ein Testing-Framework für die aufstrebende serverseitige JavaScript-Plattform node.js. Seine Syntax soll ähnlich sprechend wie die von RSpec sein (siehe oben).
should.js erweitert den obersten Prototyp Object.prototype
um den Getter should
. Dieser gibt ein Objekt zurück, welches viele weitere Gettern besitzt. Dadurch kann man auf die runden Klammern für Methodenaufrufe (den sogenannten Call Operator) verzichten. Die Kernelemente von Tests, sogenannte Assertions, lassen sich damit sehr kurz und lesbar notieren. Ein paar Beispiele:
var user = { name: 'Ada', age: 46 }; user.name.should.be.a('string') user.name.should.equal('Ada') user.should.not.have.property('age', 35)
Verglichen mit anderen Testing-Frameworks ist diese Syntax sehr »fluent«:
// Verbreitete Assertion-Syntax assertEquals(user.name, 'Ada') // Node.js Assert-Modul und darauf aufbauende Frameworks assert.equal(user.name, 'Ada') // Jasmine expect(user.name).toEqual('Ada') // Nodeunit test.equal(user.name, 'Ada')
Einen ähnlichen Schritt geht JSpec, welches für Testcases eine DSL definiert, die stark an Ruby und RSpec erinnert. Allerdings ist diese DSL selbst nicht JavaScript, sondern wird geparst und in JavaScript übersetzt.
Links und Quellen
- Weblog von Brendan Eich, dem Erfinder von JavaScript und Mitentwickler an ECMAScript
- A Minute With Brendan, regelmäßiger Podcast mit Brendan Eich
- ECMA-262-5 in detail. Chapter 1. Properties and Property Descriptors. von Dmitry Soshnikov
- ECMAScript 5 Compatibility Table von kangax
- Say Hello to ECMAScript 5. Präsentationsfolien von kangax
- ECMAScript.Next. Vortrag von David Herman
- Proxies are awesome. Vortrag von Brendan Eich auf der JSConf.eu
- Tutorial zu Harmony Proxies von Tom Van Cutsem
- Harmony-Proxies im Wiki der ECMAScript-Arbeitsgruppe
Danksagung
Dank geht an Tim Tepaße (@ttepasse) für die Erlaubnis, seine Beispiele in einem längeren Artikel auszuarbeiten. Danke auch an Sebastian Deutsch (@sippndipp) für den Hinweis auf should.js.