sap01: sort() -Problem

Hallo,

<html><body>
<script type="text/javascript">
function Numsort (a, b) {
  return a - b;
}

var Zahlen = new Array(27, 2, 10, 4);
Zahlen.sort(Numsort);

var Zahlenausgabe = Zahlen.join(",");

document.write(Zahlenausgabe);
</script>
</body></html>

Das obige Beispiel stammt von ...
http://de.selfhtml.org/javascript/objekte/array.htm
-Es sortiert Zahlen, und funktioniert 1A.

Mein Problem: Ich habe eine Situation mit drei Links, deren
Klickhäufigkeit in folgendem String gespeichert ist:

var Sammlung = "LinkA=4; LinkB=13; LinkC=21";

Wie kriegt man es (z.B. mittels einer for/next Schleife) hin, dass eine
Messagebox aufpoppt, welche
den 1.häufigst geklickten Link + den String "LinkC" aussgibt, dann
den 2.häufigst geklickten Link + den String "LinkB" aussgibt, und dann
den 3.häufigst geklickten Link + den String "LinkA" aussgibt

Das Folgende Beispiel ist ein ungefähres Muster und funzt aber noch nicht:

for (var i = 0; i < 3; i++)
{
alert(Linkname[i]+"wurde"+Klickhäufigkeit[i]+"x geklickt");
}

Wie mach' ich das?

  1. Hallo,

    var Sammlung = "LinkA=4; LinkB=13; LinkC=21";

    Wie kriegt man es (z.B. mittels einer for/next Schleife) hin, dass eine
    Messagebox aufpoppt, welche
    den 1.häufigst geklickten Link + den String "LinkC" aussgibt, dann
    den 2.häufigst geklickten Link + den String "LinkB" aussgibt, und dann
    den 3.häufigst geklickten Link + den String "LinkA" aussgibt

    Wie mach' ich das?

    Wenn du die sort-Methode benutzen willst, musst du natürlich zuerst dafür sorgen, dass du die drei Informationen aus dem String in ein Array bekommst, welches du nachher sortieren kannst: [ref:self812;javascript/objekte/string.htm#split@title=Sammlung.split()] ist dein Freund.

    Zum eigentlichen Sortieren musst du eine passende Sortierfunktion haben und an die sort-Methode übergeben. Was diese Funktion zu liefern hat, steht in der Erläuterung zu [ref:self812;javascript/objekte/array.htm#sort@title=Array.sort()].

    Gruß, Don P

  2. Dieses extrem verdichtete Beispiel ...

    ~~~javascript var histogram = "LinkA=4; LinkB=13; LinkC=21";

    histogram
        .split("; ")
        .map(function (str) {
          var record = str.split("=");
          return {
            "link"  : record[0],
            "count" : Number(record[1])
          };
        })
        .sort(function (a, b) {
          return b.count - a.count;
        })
        .slice(0, 3)
        .forEach(function (record, index) {
          console.log(  String(index+1)
                      + " - "
                      + record.link
                      + " - "
                      + String(record.count));
        });

      
    ... ergibt diese Ausgabe:  
      
      1 - LinkC - 21  
      2 - LinkB - 13  
      3 - LinkA - 4  
      
    Allerdings ist das nun sehr verdichtet und funktioniert auch nur in moderneren Browsern. Ich drösel das mal auf und gebe konventionellere Beispiele.  
      
    Im wesentlichen geht es hier um das Umwandeln von verschachtelten Datentypen. Die Variable `histogram`{:.language-javascript} ist ein String. Strings kann man nicht gut mit Bordmitteln sortieren, also wird der String in ein Array umgewandelt. Strings haben die Methode [split()](http://de.selfhtml.org/javascript/objekte/string.htm#split), die einen String an bestimmten Stellen aufteilt und die Teile innerhalb eines Arrays zurück gibt.  
      
      `var arrayOfStrings = histogram.split("; ");`{:.language-javascript}  
      
    ... spaltet Deinen String immer dort, wo ein Semikolon und ein Leerzeichen auftauchen und gibt dieses zurück:  
      
      `["LinkA=4", "LinkB=13", "LinkC=21"]`{:.language-javascript}  
      
    Das Problem bei diesem Array ist, dass es immer noch Strings enthält und die URLs und die Klickraten immer noch in diesem String zusammenhängen und die Klickraten auch noch als Strings und nicht als Zahlen vorliegen. Man muss also weiterarbeiten. So erklärt das auch die leicht ungewöhnliche Schreibweise im obigen Beispiel, die mit den jeweiligen Methodenaufrufen in der nächsten Zeile. Das ist ein verbundenes Statement, das immer auf den Rückgabewert der vorherigen Methode operiert. Konventionell sähe das so aus:  
      
      
      ~~~javascript
    var arrayOfObjects = [],  
          str, arrayOfTwoStrings, link, countAsString, count, newObj;  
      
      for (var index = 0, end = arrayOfStrings.length; index < end; index++) {  
      
        // aktueller String aus dem Array holen  
        str = arrayOfStrings[index];  
      
        // Wieder aufsplitten, diesmal anhand des Gleichheitszeichens  
        arrayOfTwoStrings = str.split("=");  
      
        // Link aus dem neuen Array rausholen  
        link = arrayOfTwoStrings[0];  
      
        // dito die Klickrate  
        countAsString = arrayOfTwoStrings[1];  
      
        // ... die aber noch in eine Zahl statt eines Strings umgewandelt werden sollte  
        count = Number(countAsString);  
      
        // An die gleiche Stelle im Resultatsarray arrayOfObjects passt nur ein Ding  
        // also erstelle ich ein neues Objekt  
        newObj = {};  
      
        // ... weise diesem den Link zu  
        newObj.link = link;  
      
        // ... und die Klickrate  
        newObj.count = count;  
      
        // ... und packe es in das neue Array:  
        arrayOfObjects[index] = newObj;  
      }
    

    Ok, das ist eklig ausführlich, dies ist gleichwertig:

    ~~~javascript var arrayOfObjects = [],
          str, record, newObj;

    for (var index = 0, end = arrayOfStrings.length; index < end; index++) {

    // aktueller String aus dem Array holen
        str = arrayOfStrings[index];

    record = str.split("=");
        newObj = {
          "link"  : record[0],
          "count" : Number(record[1])
        };

    // ... und das neu erstellte Objekt wieder in das neue Array packen
        arrayOfObjects[index] = newObj;
      }

      
    Du siehst in der Mitte, dass die eigentliche Arbeit nur 5 Zeilen sind. Dieses Muster – einen Codeschnipsel für alle Elemente eines Arrays anwenden und in ein neues oder gleiches Array packen – kommt sehr oft vor. Deswegen gibt es in vielen Programmiersprachen eine Funktion oder Methode namens map, so auch inzwischen in modernen Browsern in Javascript als [Array.prototype.map](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map), d.h. als Methode von Array-Objekten. Map wird eine Funktion als Callback übergeben und führt diese Funktion für jedes Element des Arrays aus. Dieser Funktion wird das Element übergeben, der Rückgabewert der Funktion kommt dann wieder ins Array. Array.prototype.map existiert nur in modernen Browsern, dort kann man das dann so schreiben ...  
      
      ~~~javascript
    function convert (element) {  
        var record = str.split("=");  
        return {  
          "link"  : record[0],  
          "count" : Number(record[1])  
        };  
      }  
      
      var arrayOfObjects = arrayOfStrings.map(convert);
    

    ... und muss sich nicht mit Schleifen rumschlagen. Oder eben als anonymes Funktionsobjekt wie ganz oben. Jetzt sind die Daten endlich so zubereitet, dass man sie nutzen kann. Die Variable arrayOfObjects zeigt jetzt auf diese Datenstruktur:

    ~~~javascript [ { "count" : 4, "link" : "LinkA"},
        { "count" : 13, "link" : "LinkB"},
        { "count" : 21, "link" : "LinkC"} ]

      
    Ein Array mit einem Objekt-Element für jeden Datensatz, ein Objekt besteht aus dem Link als String und der Klickrate als echte Zahl. Jetzt kann sortiert werden:  
      
      ~~~javascript
    function compare (a, b) {  
        // Hier werden nur die Klickraten-Zahlen zweier Objekte verglichen  
        // Und weil Du ab- statt aufsteigend sortieren willst, wird b von a  
        // statt wie sonst a von b subtrahiert  
        return b.count - a.count;  
      }  
      
      var sortedArray = arrayOfObjects.sort(compare);
    

    Dort hätten wir dann diese Struktur:

    ~~~javascript [ { "count" : 21, "link" : "LinkC"},
        { "count" : 13, "link" : "LinkB"},
        { "count" : 4, "link" : "LinkA"} ]

      
    Das hier ist im Beispiel eigentlich unnötig, weil Du schon drei Einträge hast. aber kann man gleich auf die Top 3 limitieren mit der Array-Methode [.slice()](http://de.selfhtml.org/javascript/objekte/array.htm#slice),die ein Teilarray zurück gibt, hier drei Elemente vom Index 0 an gezählt:  
      
      `var top3 = sortedArray.slice(0, 3);`{:.language-javascript}  
      
    Und jetzt muss man das nur noch ausgeben, klassisch mit einer Schleife:  
      
      ~~~javascript
    for (var index = 0, end = top3.length, element; index < end; index++) {  
      
        // aktuelles Element aus dem Array holen  
        element = top3[index];  
      
        // Und was damit tun, inkl. Typumwandlung  
        console.log(  String(index+1)  
                    + " - "  
                    + element.link  
                    + " - "  
                    + String(element.count));  
      }
    

    Du siehst hier wieder ein Muster wie bei Map oben. In modernen Browsern gibt es bei Arrays die Methode .forEach(), die wieder mit einer Funktion als Callback arbeitet. So verwende ich die dann oben. Diese Methoden führen für einen auch den null-basierten Index, wie Du siehst.

    console.log ist für Entwickler eine nettere Ausgabe in Firebug oder Safaris/Chromes Web Inspector, in deren interaktiver JS-Konsole man so etwas leicht ausprobieren kann.

    ...

    Würde ich mehr die Schleifen oder mehr Methoden wie map/forEach verwenden? Ich mag letzten Stil mehr; je mehr man sich dran gewöhnt, desto lieber wird der Stil einem. Aber ich würde das wohl nicht so verdichtet als einzigen Ausdruck schreiben wie oben sondern hier und da wohl eine Zwischenvariable opfern. Und für ältere Browser würde ich Methoden map, each, filter, etc entweder selber nachrüsten oder aber aus einer Bibliothek bekommen wie z.B. jQuery.each und jQuery.map. Über diese Problemmuster stolpert man nämlich immer wieder.

    --
    Manchmal hab ich diese dämliche Lust auf zu ausführliche Tutorials ...
    1. Danke für das hilfreiche Posting. Der Grund, warum
      der histogram-String Semikolons & SPACEs enthält ...
      "LinkA=4; LinkB=13; LinkC=21"
      ... ist, weil ich die Klickzahlen beim Klick auf den Link
      in einem Cookie speichere, also ungefähr wie folgt:

      function CookieSchreiben(aaa)
         {
            zaehler++;
            document.cookie=aaa.id+"="+zaehler
         }
      <body>
      <a href="#" id="LinkA" OnClick="CookieSchreiben(this)">ersterLink</a><br>
      </body>
      </html>

      -Ist es klug, den Linknamen & Klickzahl so zu speichern, wie ich
      es momentan mache, oder würdest du raten, es in einem
      anderen Format zu speichern?
      -Wie ich an deinem Beispiel sehe, ist das Extrahieren & Sortieren ja
      momentan einigermassen umfangreich, und man könnte
      das Cookie-Abspeichern vielleicht etwas besser machen.

      1. -Ist es klug, den Linknamen & Klickzahl so zu speichern, wie ich
        es momentan mache, oder würdest du raten, es in einem
        anderen Format zu speichern?

        Jain. Deine grundsätzliche Idee ist schon praktikabel. Ich würde es aber entweder sauberer (technischer Fachbegriff) oder aber gleich mit JSON machen.

        Was heisst sauberer? Du verwendest in Deinem Cookie-String die Trennzeichen "=" und ";". Beides sind Zeichen, die in URLs vorkommen können. Bei einem split kann dann die URL statt des Cookie-Strings getrennt werden und einem hinterher die Daten durcheinander kommen. Die Lösung ist es, die Trennzeichen vorher in der URL zu maskieren, d.h. durch andere Strings zu ersetzen. Ehe Du Dir vor den Kopf schlägst ... da gibt es schon was Praktisches in Javascript mitgeliefert: encodeURIComponent().

        Ich bastel hier mal einen Klicktracker, wie ich ihn unter diesem Aspekt halbwegs implementieren würde:

        ~~~javascript // Der Klicktracker ist eigentlich nur ein Objekt in dem Daten gespeichert
          // werden sollen, wie z.B. hinterher { "LinkA" : 4 }
          // Auch wenn er ein Singleton ist, bauen wir dennoch eine Konstruktur-
          // Funktion, die die Intialisierung übernimmt.

        function Klicktracker () {

        // Wir brauchen später einen Verweis auf das Objekt selber.
              var self = this;

        // Hier werden die Daten gespeichert
              this.storage = {}

        // Beim Start ruft der Klicktracker die Methode .restore() auf, die
              // den Klicktracker mit Werten befüllt
              this.restore();

        // Dann wird ein Ereignis für das load-Event des Dokument registriert.
              // Bei der Auslösung dieses Ereignisses wird für alle Links des
              // Dokumentes eine Handler-Funktion registriert, die wiederum die
              // eigene Methode .klick() aufruft.

        // Mehr zur Funktion addEvent() später
              addEvent(window, "load", function () {

        // Erst wenn das Dokument geladen ist, kann man alle Links auf
                  // einmal sammeln
                  var links = document.getElementByTagName("a");

        // Durchlaufen ...
                  for (var i = 0, end = links.length; i < end; i++) {

        var link = links[i];

        // wieder: mehr zu AddEvent später
                      addEvent(link, "click", function (e) {

        // Ignorier diese beiden Zeilen erstmal
                          e = e || window.event;
                          var target = e.target || e.srcElement;

        // Wenn geklickt wird, wird die eigene Methode .klick()
                          // mit dem geklickten Element als Argument aufgerufen.
                          self.klick(target);
                      });
                  }
              });
          }

        // Bedient wird der Klicktracker über Methoden, die an den Prototypen
          // des Objekts kommen.

        // Die Methode .klick() wird aufgerufen, wenn ein Link geklickt wird.
          // Argument ist das geklickte Element
          Klicktracker.prototype.klick = function (target) {

        // target zeigt auf das Element, dass geklickt wurde, in diesem Fall
              // also das a-Element. Dadurch kriegen wir die Link-Adresse raus:
              var link = target.href;

        // Wenn der Link schon im Klicktracker-Objekt vorhanden ist, addiere
              // eins dazu, ansonsten setze den Eintrag im Objekt neu an und zwar
              // auf eins.
              if (link in this) {
                  this.storage[link]++;
              } else {
                  this.storage[link] = 1;
              }

        // Da wir die Seite gleich verlassen werden, speicher den Zustand des
              // Klicktracker-Objektes
              this.safe();
          }

        // Die Methode .safe() wandelt das Storage-Objekt in einen geeigneten
          // String um und speichert diesen.
          Klicktracker.prototype.safe = function () {

        // Erstmal überprüfen, on es was zu speichern gibt. Wenn nicht, dann
              // halt nicht. isEmptyObject() ist wieder so eine Helferfunktion.
              if (isEmptyObject(this.storage)) {
                  return;
              }

        // Jetzt wird wieder massiv die Daten umgewandelt. Erstmal ein map vom
              // Storage-Objekt auf ein Array
              var arrayOfEncodedStrings = [];
              for (var link in this.storage) {

        // Sicherheitsabfrage
                  if (this.storage.hasOwnProperty(link)) {

        // der Link wird maskiert.
                      link = encodeURIComponent(link);

        // Die Klickrate wird in einen String umgewandelt.
                      var count = encodeURIComponent(this.storage[link].toString());

        // ... und das ganze wird gespeichert mit "=" als Trennzeichen.
                      arrayOfEncodedStrings.push(link + "=" count);
                  }
              }

        // Das Array von Strings wird jetzt in einen String mit dem Trennzeichen
              // "+" umgewandelt. Arrays haben dafür die Methode .join
              var cookieString = arrayOfEncodedString.join("+");

        // Uuund speichern
              document.cookie = cookieString;
          }

        // Die Methode .restore() wird beim Laden aufgerufen und befüllt das interne
          // Storage-Objekt mit den Daten aus dem Cookie-String
          Klicktracker.prototype.restore = function () {
              var cookieString = documentCookie;
              var arrayOfStrings = cookieString.split("+");

        for (var i = 0, end = arrayOfStrings.length; i < end; i++) {
                  var currentString = arrayOfStrings[i];
                  var currentArray = currentString.split("=");

        // wieder brav decoden
                  var link  = decodeURIComponent(currentArray[0]);
                  var count = decodeURIComponent(currentArray[1]);

        // und umwandeln
                  count = Number(count);

        // Und speichern
                  this.storage[link] = count;
              }
          }

        // Und nun den Klicktracker starten und um den globalen Namensraum nicht
          // zu verschmutzen als Eigenschaft direkt an der Konstrukturfunktion
          // speichern.
          Klicktracker.tracker = new Klicktracker();

          
        Ok, das ist sehr viel, aber beachte, dass das zu pädagogischen Zwecken sehr durchkommentiert ist und ausführlicher ist, als ich das in der Praxis schreiben würde.  
          
          
        JSON ist ein String-Format, mit dem man verschachtelte Javascript-Datenstrukturen in einen String und wieder heraus bekommen kann. Es war lange ein Quasi-Standard; inzwischen gibt es in modernen Browsern schon Methoden dafür. Wenn die nicht vorhanden sind, bindet man die [quasi-offizielle Bibliothek](https://github.com/douglascrockford/JSON-js/blob/master/json2.js) mit ein. Bei Vorhandensein des JSON-Objektes kann man dann die safe- und restore-Methoden so schreiben:  
          
          ~~~javascript
        Klicktracker.prototype.safe = function () {  
              if (isEmptyObject(this.storage)) {  
                  document.cookie = "";  
              } else {  
                  document.cookie = JSON.stringify(this.storage);  
              }  
          }  
          
          Klicktracker.prototype.restore = function () {  
              if (!document.cookie) {  
                  this.storage = {};  
              } else {  
                  this.storage = JSON.parse(document.cookie);  
              }  
          }
        

        Was netter ist. Vorausgesetzt man hat die JSON-Funktionen zur Verfügung.

        Apropos Voraussetzungen. Ich benutze da oben Helfer-Funktionen. isEmptyObject ist so definiert:

        ~~~javascript function isEmptyObject (obj) {
              for (var temp in obj) {
                  return false;
              }
              return true;
          }

          
        [addEvent()](http://molily.de/js/event-handling-fortgeschritten.html#addevent-helfer) ist eine sehr verbreitete Helferfunktion, die das unterschiedliche Event-Handling älterer Browserversionen vereinheitlicht und es dennoch schafft, dass man verschiedene Event-Handler an ein Element packen kann. Best practice, sozusagen. Molilys [Javascript-Einführung](http://molily.de/js/) ist zum Thema Event-Handling sowieso lesenswert und dringst Du bis Abschnitt 16.4 vor, erkennst Du auch, warum in der Konstruktor-Funktion der Umweg mit dem self nötig war bzw. dass ich bindAsEventListener hätte nehmen sollen, wäre es denn vorhanden und müsse man sich das nicht selbst doch noch definieren.  
          
        Du merkst hier ein unterschwelliges Thema: Javascript wird kürzer und verständlicher, wenn man andere Funktionen hat, die z.B. mittels einer Bibliothek eingebunden werden. jQuery z.B. hat unter anderem Unterstützung für Event-Handling und anderes praktisches. JSON braucht aus absurden Gründen (ernsthaft: jQuery hat $.parseJSON aber kein Stringify?) wieder obige Library. Hier mal ein kompletter auf jQuery aufbauender Klicktracker:  
          
          ~~~javascript
        jQuery(function ($) {  
              var storage;  
          
             // Restore  
             if (!document.cookie) {  
                 storage = {};  
             } else {  
                 storage = JSON.parse(document.cookie);  
             }  
          
             $("a").click(function () {  
          
                 // Adding a click  
                 var link = this.href;  
                 if (link in storage) {  
                     storage[link]++;  
                 } else {  
                     storage[link] = 1;  
                 }  
          
                 // Saving the Cookie  
                 document.cookie = JSON.stringify(storage)  
          })
        

        -Wie ich an deinem Beispiel sehe, ist das Extrahieren & Sortieren ja
        momentan einigermassen umfangreich, und man könnte
        das Cookie-Abspeichern vielleicht etwas besser machen.

        Ach. Geht. Für die Zukunft sind mit der Weiterentwicklung von HTML (5) bessere Speichermöglichkeiten (Dom Storage, IndexedDB ...) geplant, aber die alten Browser mal wieder. Es gibt auch noch andere Hacks, wie z.B. das Speichern in window.name, aber das ist ja nur auf die Session des Browsers begrenzt. Wenn das jedoch Dein Fall ist, dann guck Dir mal molilys Session Storage Wrapper (Dokumentation) an, da wird aus unterschiedlichen Möglichkeiten die beste ausgewählt und vieles automatisiert.

    2. Hallo,

      Wow, Kompliment! Das hast du wirklich sehr lehrreich abgehandelt.

      die leicht ungewöhnliche Schreibweise im obigen Beispiel, die mit den jeweiligen Methodenaufrufen in der nächsten Zeile

      wird z.B. überall bei protovis benutzt. Dort nennen sie es "function chaining".

      Gefällt mir irgendwie und ich überlege, ob ich das Mauster nicht auch für eigene Scripts gebrauchen kann. Der Code kann so übersichtlicher werden, weil die ständige Wiederholung von Bezeichnern entfällt. Man notiert z.B.

        
      var youngest = gf1.mybaby()  
        
        .born(new Date())  
        .name(undefined)  
        .height(0.55)  
        .weight(3.5)  
        .gender('f')  
        .hair('none')  
        .eyes({left:'blue',right:'brown'});  
      
      

      anstelle von

        
      var youngestKid = gf1.myBaby;  
        
      youngest.born = new Date();  
      youngest.name = undefined;  
      youngest.height = 0.55;  
      youngest.weight = 3.5;  
      youngest.gender = 'f';  
      youngest.hair = 'none';  
      youngest.eyes = {left:'blue',right:'brown'};  
      
      

      Einzelnen Methoden eines Objekts geben jeweils das ganze Objekt zurück, so dass man sie z.B. beim Setzen von Eigenschaften einfach aneinander ketten kann. :)

      Gruß, Don P

      1. 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.
        
        1. [X] Fachlich hilfreich!

          Man könnte sich fast eine ES5-Library basteln, die einem bessere domain-specific languages erlaubt.

          Sehr richtig, das wird teilweise auch schon getan. Siehe etwa das Testing-Framework https://github.com/visionmedia/should.js für node.js. Dort wird ebenfalls Object.prototype mit einem Getter »should« erweitert, der ein Objekt mit weiteren Gettern zurückgibt. Dadurch kann man auf die Klammern für Methodenaufrufe verzichten, sodass man schreiben kann:

          user.should.not.have.property('age', 0)
          true.should.be.true
          'test'.should.equal('test')
          'test'.should.be.a('string')

          Ist das nicht geil? Das fühlt sich viel besser an als das ganze assertEquals(x, y)- oder expect(x).toEqual(y)-Gefrickel von anderen Unit-Testing-Bibliotheken. Fast wie bei Ruby/RSpec. ;)

          Mathias

    3. Hallo Tim,

      ein wunderbarer Beitrag. Danke!

      Freundliche Grüße

      Vinzenz