Tim Tepaße: Hexerei mit ECMAScript 5 und Harmony: Ein Ersatz für with

Beitrag lesen

die leicht ungewöhnliche Schreibweise [..]
wird z.B. überall bei protovis benutzt. Dort nennen sie es "function chaining".

Oder method chaining und neulich habe ich den Begriff Fluent Interface dafür gelesen. Wobei ich für Dein Beispiel ja das eher in den Konstruktor verlegen würde:

~~~javascript var youngest = new Baby({
      born   : new Date(),
      name   : "Ada",
      height : 0.55,
      weight : 3.5,
      gender : "f",
      hair   : null,
      eyes   : {
          left  : "blue",
          right : "brown"
      }
  })

  
Aber Du hast natürlich recht: bei darauffolgenden mehreren Änderungen ist das nervig. Eigentlich will man etwas wie das [with statement](http://de.selfhtml.org/javascript/sprache/objekte.htm#with), aber dieses hat die klassischen Scope-Probleme, weswegen man es besser nicht einsetzt.  
  
In dem [Artikel](http://banksimple.com/engineering/2011/02/25/the-advantages-of-green-fields-taming-effectful-programming/), in dem ich den Begriff Fluent Interface gelesen habe und den ich dafür noch mal rausgesucht habe, ist auch ein nettes Beispiel das Closures [doto-Macro](http://clojure.org/java_interop#Java%20Interop-The%20Dot%20special%20form-(doto%20instance-expr%20(instanceMethodName-symbol%20args*)*)) aufzeigt. Ich habe da mal wieder die syntaktischen Möglichkeiten von Lisp-Dialekten bewundert. Zurück zu Javascript brachte mich das auf die Idee, auch dort mal für alle Objekte leichtere Möglichkeiten zum Property-Setzen bzw. Methoden-Aufrufen einzuführen.  
  
  
Variante 1: Dank ECMAScript 5 kann man inzwischen wunderbar Object.prototype erweitern:  
  
  ~~~javascript
Object.defineProperty(Object.prototype, "setProperties", {  
      "value" : function (props) {  
          var self = this  
          Object.keys(props)  
              .filter(props.hasOwnProperty, props)  
              .forEach(function (name) {  
                  self[name] = props[name]  
              })  
      }  
  })  
  
  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"  
      }  
  })

Variante 2: Wenn wir schon im wunderbaren Land von ECMAScript 5 sind, können wir auch gleich accessor functions nutzen, auch wenn die syntaktische Erleichterung nur marginal ist.

~~~javascript Object.defineProperty(Object.prototype, "properties", {
      "set" : function (props) {
          var self = this
          Object.keys(props)
              .filter(props.hasOwnProperty, props)
              .forEach(function (name) {
                  self[name] = props[name]
              })
      }
  })

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"
      }
  }

  
  
Variante 3: Der Trick beim Chaining ist ja einfach nur, immer das gleiche Objekt zurück zu geben. D.h. man könnte einfach für alle Objekte eine Methode, bzw. einen Getter definieren, der einem ein Proxy-Objekt zurück gibt, dessen Methoden  neben dem Aufruf der Original-Methode dieses tun:  
  
  ~~~javascript
Object.defineProperty(Object.prototype, "chaining", {  
      "get" : function () {  
  
          if ("_proxy" in this) {  
              return this["_proxy"]  
          }  
  
          var original = this,  
              proxy    = {};  
  
          for (var propName in this) {  
              if (original[propName] && original[propName].call) {  
                  (function closure (propName) {  
                      proxy[propName] = function () {  
                          original[propName].apply(original, arguments)  
                          return proxy  
                      }  
                  })(propName)  
              }  
          }  
  
          Object.defineProperty(this, "_proxy", {  
              "value" : proxy  
          })  
  
          return proxy  
      }  
  })  
  
  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"  
     });

Für Ausprobierer sei gewarnt, dass diese Lösung bei DOM-Objekten noch Probleme hat. Ich hatte keine Lust, das zu debuggen. Ihr vielleicht?

Variante 4: Das dynamische Erstellen des Objektes und seiner Methoden hat das Problem, dass das missachtet, dass Javascript eine sehr dynamische Sprache ist, bei der Objekte auch im Nachhinein noch Methoden bekommen können. Und wenn man auf das Caching verzichtet wird bei jedem Aufruf von chaining ein neues Objekt konstruiert. Besser wäre es, wenn man jeden beliebigen Funktionsaufruf eines Objektes abfangen könnte.

Der Nachfolger von ECMAScript 5, ECMAScript Harmony, plant dafür praktischerweiser ein Konzept: richtige Proxies. Firefox 4β hat die Proxy API schon mal implementiert, folgendes Beispiel funktioniert also nur da.

~~~javascript Object.defineProperty(Object.prototype, "chaining", {
      "get" : function () {

if ("_proxy" in this) {
              return this["_proxy"]
          }

var original = this;

var proxy = Proxy.create({
              "get" : function (receiver, propName) {
                  return function () {
                      original[propName].apply(original, arguments)
                      return this
                  }
              }
          })

Object.defineProperty(this, "_proxy", {
              "value" : proxy
          })

return proxy

}
  })

  
(Für einen richtigen Proxy-Handler sollte man noch die fundamental traps implementieren.)  
  
  
Langfristig in die Zukunft geguckt, macht Javascript sehr viel Spaß. Man könnte sich fast eine ES5-Library basteln, die einem bessere domain-specific languages erlaubt.