3 Funktionen

Funktionen sind die Arbeitstiere von JavaScript. Sie dienen sowohl als die wichtigste Abstraktionsmöglichkeit für Programmierer wie auch als Implementierungsmechanismus. Dabei übernehmen sie Rollen, für die in anderen Sprachen mehrere verschiedene Merkmale vorhanden sind: Prozeduren, Methoden, Konstruktoren und sogar Klassen und Module. Wenn Sie erst einmal mit den Feinheiten von Funktionen vertraut sind, haben Sie bereits einen Großteil von JavaScript gemeistert. Die Kehrseite der Medaille allerdings ist, dass es eine gewisse Zeit dauern kann, um zu lernen, wie Sie Funktionen in den verschiedenen Zusammenhängen jeweils effektiv einsetzen.

Thema 18 Die Unterschiede zwischen Funktionen, Methoden und Konstruktoren

Wenn Sie mit der objektorientierten Programmierung vertraut sind, sehen Sie Funktionen, Methoden und Klassenkonstruktionen wahrscheinlich als drei unterschiedliche Dinge an. In JavaScript handelt es sich dabei aber nur um drei verschiedene Verwendungsarten desselben Konstrukts, nämlich der Funktionen.

Die einfachste Verwendungsart ist der Funktionsaufruf:

Funktionen

function hello(username) {
   return "hello, " + username;
}
hello("Keyser Söze"); // "hello, Keyser Söze"

Dies funktioniert genau so, wie es aussieht: Die Funktion hello wird aufgerufen, und der Parameter username wird an das übergebene Argument gebunden.

Methoden

Methoden sind in JavaScript nichts anderes als Objekteigenschaften, die zufällig auch Funktionen sind:

var obj = {
   hello: function() {
      return "hello, " + this.username;
   },
   username: "Hans Gruber"
};
obj.hello(); // "hello, Hans Gruber"

Beachten Sie, dass hello auf this verweist, um auf die Eigenschaften von obj zuzugreifen. Sie mögen vielleicht annehmen, dass this an obj gebunden wird, da die Methode hello in obj definiert ist. Wir können aber einen Verweis auf diese Funktion in ein anderes Objekt kopieren, und dann erhalten wir eine andere Antwort:

var obj2 = {
   hello: obj.hello,
   username: "Boo Radley"
};
obj2.hello(); // "hello, Boo Radley"

Tatsächlich geschieht in einem Methodenaufruf Folgendes: Der Aufrufausdruck selbst bestimmt die Bindung von this, die auch als Empfänger des Aufrufs bezeichnet wird. Der Ausdruck obj.hello() schlägt also die Eigenschaft hello von obj nach und ruft sie mit dem Empfänger obj auf. Dagegen schlägt der Ausdruck obj2.hello() die Eigenschaft hello von obj2 nach – die zufällig dieselbe Funktion ist wie obj.hello –, ruft sie aber mit dem Empfänger obj2 auf. Allgemein gesagt, wird beim Aufruf einer Methode für ein Objekt die Methode nachgeschlagen und dann das Objekt als Empfänger der Methode verwendet.

Da Methoden nichts anderes sind als Funktionen, die für ein bestimmtes Objekt aufgerufen werden, gibt es keinen Grund dafür, dass nicht auch normale Funktionen auf this verweisen könnten:

function hello() {
   return "hello, " + this.username;
}

Das ist nützlich, um eine Funktion für die gleichzeitige Verwendung durch mehrere Objekte vorzudefinieren:

var obj1 = {
   hello: hello,
   username: "Gordon Gekko"
};
obj1.hello(); // "hello, Gordon Gekko"
var obj2 = {
   hello: hello,
   username: "Biff Tannen"
};
obj2.hello(); // "hello, Biff Tannen"

Es ist jedoch nicht besonders sinnvoll, eine Funktion, die this verwendet, als Funktion statt als Methode aufzurufen:

hello(); // "hello, undefined"

Ungünstigerweise stellt ein solcher Funktionsaufruf anstelle eines Methodenaufrufs das globale Objekt als Empfänger bereit, das in diesem Fall keine Eigenschaft namens username hat und daher undefined ausgibt. Der Aufruf einer Methode als Funktion ist selten sinnvoll, wenn die Methode this verwendet, da es keinen Grund gibt, anzunehmen, dass das globale Objekt die Erwartungen erfüllt, die die Methode von dem Objekt hat, für das sie aufgerufen wird. Die Bindung an das globale Objekt ist so problematisch, dass die Standardbindung von this im strengen Modus von ES5 auf undefined geändert wird:

function hello() {
   "use strict";
   return "hello, " + this.username;
}
hello();     // Fehler: Die Eigenschaft "username" von undefined
             // kann nicht gelesen werden

Dadurch wird die versehentliche falsche Behandlung von Methoden als einfache Funktionen erschwert, da der Versuch, auf Eigenschaften von undefined zuzugreifen, sofort zu einem Fehler führt.

Konstruktoren

Die dritte Verwendungsart von Funktionen ist die als Konstruktoren. Ebenso wie Methoden und einfache Funktionen werden auch Konstruktoren mit function definiert:

function User(name, passwordHash) {
   this.name = name;
   this.passwordHash = passwordHash;
}

Wenn Sie User mit dem Operator new aufrufen, wird die Funktion als Konstruktor behandelt:

var u = new User("sfalken", "0ef33ae791068ec64b502d6cb0191387");
u.name; // "sfalken"

Anders als bei Funktions- und Methodenaufrufen wird bei Konstruktoraufrufen ein brandneues Objekt als Wert von this übergeben und das neue Objekt implizit als Ergebnis zurückgegeben. Die wichtigste Rolle von Konstruktorfunktionen besteht in der Initialisierung des Objekts.

Thema 19 Keine Angst vor Funktionen höherer Ordnung

Der Begriff Funktionen höherer Ordnung war einmal das Erkennungswort der Eingeweihten aus dem Zirkel der funktionalen Programmierung, eine esoterische Bezeichnung für etwas, das eine fortgeschrittene Programmiertechnik zu sein schien. Nichts könnte aber weiter von der Wahrheit entfernt sein. Die prägnante Eleganz von Funktionen zu nutzen, kann zu einfacherem und kompakterem Code führen. Im Laufe der Jahre haben Skriptsprachen diese Techniken übernommen und damit den Schleier des Geheimnisvollen gelüftet, der die besten Idiome der funktionalen Programmierung umgab.

Callback-Funktionen

Funktionen höherer Ordnung sind nichts anderes als Funktionen, die andere Funktionen als Argumente übernehmen oder als Ergebnis eine Funktion zurückgeben. Eine Funktion als Argument zu übernehmen (die häufig als Callback-Funktion bezeichnet wird, da sie von der Funktion höherer Ordnung »zurückgerufen« wird), ist ein besonders leistungsfähiges und ausdrucksstarkes Idiom, das in JavaScript-Programmen häufig eingesetzt wird.

Betrachten Sie die Standardmethode sort für Arrays. Damit sie bei allen möglichen Arrays funktioniert, muss der Aufrufer bestimmen, wie zwei Elemente in dem Array verglichen werden:

function compareNumbers(x, y) {
   if (x < y) {
      return -1;
   }
   if (x > y) {
      return 1;
   }
   return 0;
}
[3, 1, 4, 1, 5, 9].sort(compareNumbers); // [1, 1, 3, 4, 5, 9]

In der Standardbibliothek hätte man auch verlangen können, dass der Aufrufer ein Objekt mit einer compare-Methode übergibt, aber wenn nur eine einzige Methode benötigt wird, ist es einfacher und kürzer, direkt eine Funktion zu übernehmen. Das obenstehende Beispiel lässt sich mithilfe einer anonymen Funktion sogar noch weiter vereinfachen:

[3, 1, 4, 1, 5, 9].sort(function(x, y) {
   if (x < y) {
      return -1;
   }
   if (x > y) {
      return 1;
   }
   return 0;
}); // [1, 1, 3, 4, 5, 9]

Wenn Sie wissen, wie Sie Funktionen höherer Ordnung einsetzen, können Sie damit Ihren Code oftmals vereinfachen und auf mühselige Wiederholungen von 08/15-Code verzichten. Für viele gebräuchliche Arrayoperationen gibt es wunderbare Abstraktionen höherer Ordnung, bei denen es sich wirklich lohnt, sich mit ihnen vertraut zu machen. Betrachten Sie als Beispiel die einfache Aufgabe, ein Array aus Strings komplett in Großbuchstaben umzuwandeln. Um das in einer Schleife zu erledigen, müssen wir Folgendes schreiben:

var names = ["Fred", "Wilma", "Pebbles"];
var upper = [];
for (var i = 0, n = names.length; i < n; i++) {
   upper[i] = names[i].toUpperCase();
}
upper; // ["FRED", "WILMA", "PEBBLES"]

Bei der praktischen Arraymethode map (die in ES5 eingeführt wurde) brauchen wir uns jedoch um die Einzelheiten der Schleife keine Sorgen zu machen, sondern müssen lediglich die elementweise Umwandlung mit einer lokalen Funktion implementieren:

var names = ["Fred", "Wilma", "Pebbles"];
var upper = names.map(function(name) {
   return name.toUpperCase();
});
upper; // ["FRED", "WILMA", "PEBBLES"]

Eigene Funktionen schreiben

Wenn Sie erst einmal den Dreh heraushaben, wie Sie Funktionen höherer Ordnung verwenden, können Sie auch die Gelegenheiten erkennen, an denen es sich lohnt, eigene zu schreiben. Sichere Kennzeichen dafür, dass Ihr Code nur auf eine Abstraktion höherer Ordnung wartet, sind Vorkommen von doppeltem oder ähnlichem Code. Nehmen wir beispielsweise an, in einem Teil eines Programms wird wie folgt ein String aus allen Buchstaben des Alphabets zusammengestellt:

var aIndex = "a".charCodeAt(0); // 97

var alphabet = "";
for (var i = 0; i < 26; i++) {
   alphabet += String.fromCharCode(aIndex + i);
}
alphabet; // "abcdefghijklmnopqrstuvwxyz"

An einer anderen Stelle des Programms wird ein String aus Ziffern erstellt:

var digits = "";
for (var i = 0; i < 10; i++) {
   digits += i;
}
digits; // "0123456789"

Schließlich baut das Programm an einer dritten Stelle einen String aus zufälligen Zeichen zusammen:

var random = "";

for (var i = 0; i < 8; i++) {
   random += String.fromCharCode(Math.floor(Math.random() * 26)
                                 + aIndex);
}
random; // "bdwvfrtp" (jedes Mal ein anderes Ergebnis)

An jeder dieser drei Stellen wird ein anderer String erstellt, aber die Logik ist die gleiche. In jeder Schleife wird ein String durch die Verkettung der Ergebnisse von Berechnungen für die einzelnen Abschnitte zusammengestellt. Die gemeinsame Logik können wir in eine Hilfsfunktion auslagern:

function buildString(n, callback) {
   var result = "";
   for (var i = 0; i < n; i++) {
      result += callback(i);
   }
   return result;
}

Die Implementierung von buildString enthält alle Gemeinsamkeiten der drei Schleifen, verwendet anstelle der abweichenden Teile aber Parameter: Aus der Anzahl der Schleifeniterationen wird die Variable n, aus der Konstruktion der einzelnen Stringabschnitte ein Aufruf der Funktion callback. Mit buildString können wir nun alle drei Beispiele vereinfachen:

var alphabet = buildString(26, function(i) {
   return String.fromCharCode(aIndex + i);
});
alphabet; // "abcdefghijklmnopqrstuvwxyz"

var digits = buildString(10, function(i) { return i; });
digits; // "0123456789"

var random = buildString(8, function() {
   return String.fromCharCode(Math.floor(Math.random() * 26)
                              + aIndex);
});
random; // "ltvisfjr" (jedes Mal ein anderes Ergebnis)

Abstraktionen höherer Ordnung bieten viele Vorteile. Knifflige Teile der Implementierung, z.B. die richtigen Randbedingungen der Schleife, werden in die Implementierung der Funktion höherer Ordnung verlagert. Dadurch müssen Sie Fehler in der Logik nur an einer Stelle korrigieren, anstatt nach jedem Vorkommen des betreffenden Codemusters im gesamten Programm Ausschau zu halten. Wenn Sie die Operation effizienter gestalten wollen, müssen Sie die Änderungen ebenfalls nur an einer einzigen Stelle vornehmen. Wenn Sie der Abstraktion schließlich noch einen erläuternden Namen wie buildString geben, wird für jeden Leser deutlich, was der Code macht, ohne dass er sich die Einzelheiten der Implementierung genauer ansehen muss.

Wenn Sie Funktionen höherer Ordnung anstreben, sobald Sie feststellen, dass Sie wiederholt Code im gleichen Muster schreiben, führt das zu knapperem Code, höherer Produktivität und besserer Lesbarkeit. Gewöhnen Sie sich an, nach häufig vorkommenden Mustern Ausschau zu halten und sie in Hilfsfunktionen höherer Ordnung zu verlagern.

Thema 20 Rufen Sie Methoden mit benutzerdefiniertem Empfänger mit call auf

Der Empfänger einer Funktion oder Methode (also der Wert, der an das Schlüsselwort this gebunden wird), wird gewöhnlich durch die Syntax des Aufrufers bestimmt. Die Syntax eines Methodenaufrufs bindet das Objekt, in dem die Methode nachgeschlagen wurde, an this. Manchmal ist es jedoch nötig, eine Funktion mit einem benutzerdefinierten Empfänger aufzurufen, wobei es sein kann, dass die Funktion noch keine Eigenschaft des gewünschten Empfängerobjekts ist. Natürlich wäre es in diesem Fall möglich, die Methode als neue Eigenschaft zu dem Objekt hinzuzufügen:

obj.temporary = f;        // Was ist, wenn obj.temporary
                          // bereits existiert?
var result = obj.temporary(arg1, arg2, arg3);
delete obj.temporary;     // Was ist, wenn obj.temporary
                          // bereits existiert?

Diese Vorgehensweise ist aber unbequem und sogar gefährlich. Eine Änderung von obj ist häufig nicht wünschenswert und manchmal auch gar nicht möglich. Vor allem besteht die Gefahr, dass der Name, den Sie für die temporäre Eigenschaft wählen, in Konflikt mit einer bereits vorhandenen Eigenschaft von obj steht. Außerdem können Objekte »eingefroren« oder versiegelt sein, was die Ergänzung um neue Eigenschaften verhindert. Darüber hinaus ist es ganz allgemein kein guter Stil, willkürlich Eigenschaften zu Objekten hinzuzufügen – vor allem zu Objekten, die Sie nicht selbst erstellt haben (siehe Thema 42).

call

Glücklicherweise verfügen Funktionen über die integrierte Methode call, um einen benutzerdefinierten Empfänger bereitzustellen:

f.call(obj, arg1, arg2, arg3);

Dieser Aufruf verhält sich ähnlich wie der direkte Aufruf:

f(arg1, arg2, arg3);

In der Variante mit call wird jedoch als erstes Argument ausdrücklich ein Empfängerobjekt übergeben.

Nicht existierende Methoden aufrufen

Die Methode call ist praktisch für den Aufruf von Methoden, die entfernt, bearbeitet oder überschrieben worden sind. Thema 45 zeigt ein praktisches Beispiel, in dem die Methode hasOwnProperty für willkürliche Objekte aufgerufen werden kann, selbst für Dictionarys. In einem Dictionary-Objekt ergibt das Nachschlagen von hasOwnProperty einen Eintrag im Dictionary statt einer geerbten Methode:

dict.hasOwnProperty = 1;
dict.hasOwnProperty("foo"); // Fehler: 1 ist keine Funktion

Mit call ist es möglich, hasOwnProperty auch für ein Dictionary aufzurufen, auch wenn diese Methode nirgendwo in dem Objekt gespeichert ist:

var hasOwnProperty = {}.hasOwnProperty;
dict.foo = 1;
delete dict.hasOwnProperty;
hasOwnProperty.call(dict, "foo");             // true
hasOwnProperty.call(dict, "hasOwnProperty");  // false

Funktionen höherer Ordnung definieren

Die Methode call ist auch zur Definition von Funktionen höherer Ordnung nützlich. Ein gebräuchliches Idiom für solche Funktionen besteht darin, ein optionales Argument anzunehmen, um den Empfänger für den Aufruf der Funktion anzugeben. Ein Objekt, das eine Tabelle von Schlüssel-Wert-Bindungen darstellt, kann beispielsweise eine forEach-Methode bereitstellen:

var table = {
   entries: [],
   addEntry: function(key, value) {
      this.entries.push({ key: key, value: value });
   },
   forEach: function(f, thisArg) {
      var entries = this.entries;
      for (var i = 0, n = entries.length; i < n; i++) {
         var entry = entries[i];
         f.call(thisArg, entry.key, entry.value, i);
      }
   }
};

Dadurch können Verbraucher des Objekts eine Methode als Callback-Funktion f von table.forEach verwenden und einen sinnvollen Empfänger für die Methode bereitstellen. Beispielsweise können wir damit den Inhalt einer Tabelle bequem in eine andere kopieren:

table1.forEach(table2.addEntry, table2);

Dieser Code entnimmt die Methode addEntry aus table2 (er könnte sie auch aus Table.prototype oder table1 entnehmen). Anschließend ruft die Methode forEach wiederholt addEntry mit table2 als Empfänger auf. Beachten Sie, dass addEntry zwar nur zwei Argumente erwartet, von forEach aber mit dreien aufgerufen wird, nämlich mit dem Schlüssel, dem Wert und dem Index. Das zusätzliche Indexargument kann jedoch keinen Schaden anrichten, da es von addEntry einfach ignoriert wird.

Thema 21 Rufen Sie variadische Funktionen mit apply auf

Nehmen wir an, jemand stellt uns die Funktion average bereit, die den Durchschnitt aus einer beliebigen Anzahl von Werten berechnet:

average(1, 2, 3);                         // 2
average(1);                               // 1
average(3, 1, 4, 1, 5, 9, 2, 6, 5);       // 4
average(2, 7, 1, 8, 2, 8, 1, 8);          // 4.625

Variadische Funktionen

Dies ist ein Beispiel für eine variadische Funktion, also eine Funktion, die eine beliebige Anzahl von Argumenten annehmen kann. Eine Variante von average mit fester Argumentanzahl könnte dagegen ein einziges Argument annehmen, das ein Array aus Werten enthält:

averageOfArray([1, 2, 3]);                    // 2
averageOfArray([1]);                          // 1
averageOfArray([3, 1, 4, 1, 5, 9, 2, 6, 5]);  // 4
averageOfArray([2, 7, 1, 8, 2, 8, 1, 8]);     // 4.625

Die variadische Version ist knapper und sicherlich auch eleganter. Diese Funktionen weisen eine komfortable Syntax auf, zumindest wenn der Aufrufer im Voraus genau weiß, wie viele Argumente er zu übergeben hat, wie es in dem obenstehenden Beispiel der Fall ist. Nehmen wir aber an, wir haben ein Array aus Werten:

var scores = getAllScores();

Wie können wir den Durchschnitt dieser Werte mithilfe der Funktion average berechnen?

average(/* ? */);

apply

Glücklicherweise verfügen Funktionen über die eingebaute Methode apply, die der Methode call ähnelt, aber eigens für den hier beschriebenen Zweck da ist. Sie nimmt ein Array aus Argumenten entgegen und ruft die Funktion dann so auf, als sei jedes Element des Arrays ein einzelnes Argument des Aufrufs. Vor dem Array nimmt apply als erstes Argument noch die Bindung von this für die aufzurufende Funktion an. Da die Funktion average nicht auf this verweist, können wir dafür einfach null übergeben:

var scores = getAllScores();
average.apply(null, scores);

Wenn scores beispielsweise drei Elemente enthält, ergibt sich dasselbe Verhalten wie bei folgendem Aufruf:

average(scores[0], scores[1], scores[2]);

Die Methode apply kann auch auf variadische Methoden angewandt werden. Stellen Sie sich beispielsweise das Objekt buffer mit der variadischen Methode append vor, die Einträge zu seinem internen Zustand hinzufügt (mehr über die Implementierung von append in Thema 22):

var buffer = {
   state: [],
   append: function() {
      for (var i = 0, n = arguments.length; i < n; i++) {
         this.state.push(arguments[i]);
      }
   }
};

Die Methode append kann mit einer beliebigen Zahl von Argumenten aufgerufen werden:

buffer.append("Hello, ");
buffer.append(firstName, " ", lastName, "!");
buffer.append(newline);

Mit dem this-Argument von apply können wir append auch mit einem berechneten Array aufrufen:

buffer.append.apply(buffer, getInputStrings());

Beachten Sie, wie wichtig das Argument buffer ist: Wenn wir hier ein anderes Objekt übergeben, versucht die Methode append die Eigenschaft state des falschen Objekts zu ändern.

Thema 22 Erstellen Sie variadische Funktionen mit arguments

In Thema 21 haben Sie die variadische Funktion average kennengelernt, die eine beliebige Anzahl von Argumenten verarbeiten und deren Durchschnittswert berechnen kann. Wie aber können wir selbst eine variadische Funktion implementieren? Die Variante averageOfArray mit fester Argumentanzahl lässt sich relativ einfach schreiben:

function averageOfArray(a) {
   for (var i = 0, sum = 0, n = a.length; i < n; i++) {
      sum += a[i];
   }
   return sum / n;
}
averageOfArray([2, 7, 1, 8, 2, 8, 1, 8]); // 4.625

Die Definition von averageOfArray enthält einen formalen Parameter, nämlich die Variable a in der Parameterliste. Wenn jemand averageOfArray aufruft, dann gibt er ein einziges Argument mit (das manchmal als das tatsächliche Argument bezeichnet wird, um es deutlich vom formalen Parameter zu unterscheiden), nämlich das Array mit den Werten.

arguments

Die variadische Version sieht fast genauso aus, definiert aber keine ausdrücklichen formalen Parameter. Stattdessen nutzt sie die Tatsache aus, dass in JavaScript alle Funktionen über die implizite lokale Variable arguments verfügen. Das arguments-Objekt bildet ein arrayähnliches Interface zu den tatsächlichen Argumenten, denn es enthält die indizierten Eigenschaften für alle tatsächlichen Argumente sowie die Eigenschaft length, die angibt, wie viele Argumente bereitgestellt werden. Dadurch kann die variadische Funktion average in Form einer Schleife über alle Elemente des arguments-Objekts ausgedrückt werden:

function average() {
   for (var i = 0, sum = 0, n = arguments.length;
      i < n;
      i++) {
      sum += arguments[i];
   }
   return sum / n;
}

Variadische Funktionen bilden flexible Interfaces, denn sie können von unterschiedlichen Clients mit schwankender Zahl von Argumenten aufgerufen werden. Für sich allein genommen, stellen sie aber auch eine gewisse Unbequemlichkeit dar, denn wenn Verbraucher sie mit einem berechneten Array von Argumenten aufrufen möchten, müssen sie die in Thema 21 beschriebene Methode apply nutzen.

Zusätzliche Version mit fester Argumentanzahl

Eine gute Faustregel lautet, dass Sie immer dann, wenn Sie der Bequemlichkeit halber eine variadische Funktion verwenden wollen, auch eine Version mit fester Argumentanzahl bereitstellen sollten, die ausdrücklich ein Array entgegennehmen kann. Das lässt sich gewöhnlich ganz einfach bewerkstelligen, da Sie die variadische Funktion im Normalfall als kleinen Wrapper implementieren können, der eine Delegierung zur Version mit fester Argumentanzahl vornimmt:

function average() {
   return averageOfArray(arguments);
}

Dadurch müssen die Nutzer Ihrer Funktion nicht auf die Methode apply zurückgreifen, die schwerer lesbar ist und häufig mit Leistungsverlusten einhergeht.

Thema 23 Ändern Sie niemals das arguments-Objekt

arguments

Das arguments-Objekt sieht zwar wie ein Array aus, verhält sich aber leider nicht so. Programmierer, die mit Perl und UNIX-Shellskripts vertraut sind, benutzen gern die Technik, Elemente über den Anfang eines Arrays aus Argumenten hinaus zu »verschieben«. Die Arrays in JavaScript verfügen tatsächlich über die Methode shift, die das erste Element entfernt und alle nachfolgenden Elemente um eins nach vorn verschiebt. Da das arguments-Objekt aber keine Instanz des Standardtyps Array ist, können wir arguments.shift() nicht direkt aufrufen.

Hier könnte nun der Gedanke aufkommen, dass wir dank der Methode call die Methode shift aus einem Array entnehmen und für das arguments-Objekt aufrufen könnten. Das klingt nach einer vernünftigen Vorgehensweise, um wie folgt die Funktion callMethod zu implementieren, die ein Objekt und einen Methodennamen übernimmt und dann versucht, die Methode des Objekts für alle restlichen Argumente aufzurufen:

function callMethod(obj, method) {
   var shift = [].shift;
   shift.call(arguments);
   shift.call(arguments);
   return obj[method].apply(obj, arguments);
}

Allerdings verhält sich diese Funktion nicht im Entferntesten wie erwartet:

var obj = {
   add: function(x, y) { return x + y; }
};
callMethod(obj, "add", 17, 25);
// Fehler: Kann die Eigenschaft "apply" von undefined nicht lesen

Der Grund für diesen Fehlschlag ist, dass es sich bei dem arguments-Objekt nicht um eine Kopie der Funktionsargumente handelt. Genauer gesagt: Alle benannten Argumente sind nur Aliase der entsprechenden Indizes im arguments-Objekt. Selbst nachdem wir mithilfe von shift Elemente aus dem arguments-Objekt entfernt haben, bleibt obj also ein Alias für arguments[0] und method ein Alias für [arguments[1]. Wenn wir also scheinbar obj["add"] aus dem Objekt entnehmen, extrahieren wir in Wirklichkeit 17[25]. An dieser Stelle bricht alles zusammen: Aufgrund der impliziten Typumwandlung in JavaScript wird 17 in ein Number-Objekt konvertiert und dann wird versucht, dessen Eigenschaft "25" zu entnehmen, die es natürlich nicht gibt, was in undefined resultiert. Anschließend wird erfolglos versucht, die Eigenschaft "apply" von undefined zu entnehmen, um sie als Methode aufzurufen.

Die Moral von der Geschicht' lautet, dass die Beziehung zwischen dem arguments-Objekt und den benannten Parametern einer Funktion äußerst labil ist. Wenn Sie arguments bearbeiten, laufen Sie Gefahr, die benannten Parameter einer Funktion in Kauderwelsch zu verwandeln. Im Strict Mode von ES5 wird die Situation sogar noch komplizierter. Funktionsparameter in diesem Modus bilden keine Aliase für ihr arguments-Objekt. Diesen Unterschied können wir vorführen, indem wir eine Funktion schreiben, die ein Element von arguments ändert:

function strict(x) {
   "use strict";
   arguments[0] = "modified";
   return x === arguments[0];
}

function nonstrict(x) {
   arguments[0] = "modified";
   return x === arguments[0];
}
strict("unmodified");     // false
nonstrict("unmodified");  // true

Infolgedessen ist es viel sicherer, das arguments-Objekt niemals zu ändern. Auf eine solche Bearbeitung können Sie ganz einfach verzichten, indem Sie als Erstes die Elemente in ein echtes Array kopieren. Zur Implementierung dieses Vorgangs gibt es ein ganz einfaches Idiom:

var args = [].slice.call(arguments);

slice

Die Arraymethode slice kopiert ein Array, wenn sie ohne zusätzliche Argumente aufgerufen wird, und das Ergebnis ist eine echte Instanz des Standardtyps Array. Es ist sichergestellt, dass dieses Ergebnis keinen Alias für irgendetwas bildet und alle normalen Methoden von Array direkt zur Verfügung stellt.

Die Implementierung von callMethod können wir korrigieren, indem wir arguments kopieren. Da wir nur die Elemente hinter obj und method brauchen, können wir slice den Anfangsindex 2 übergeben:

function callMethod(obj, method) {
   var args = [].slice.call(arguments, 2);
   return obj[method].apply(obj, args);
}

Jetzt funktioniert callMethod endlich wie erwartet:

var obj = {
   add: function(x, y) { return x + y; }
};
callMethod(obj, "add", 17, 25); // 42

Thema 24 Speichern Sie Verweise auf arguments in einer Variable

Iterator

Ein Iterator ist ein Objekt, das sequenziellen Zugriff auf eine Sammlung von Daten bietet. In einer typischen API wird die Methode next bereitgestellt, um den nächsten Wert einer Folge zu liefern. Nehmen wir aber an, wir sollten eine Komfortfunktion schreiben, die eine willkürliche Anzahl von Argumenten annimmt und dafür einen Iterator erstellt:

var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // 1
it.next(); // 4
it.next(); // 1

Elemente von arguments durchlaufen

Die Funktion values muss eine beliebige Anzahl von Argumenten akzeptieren, weshalb wir unser Iteratorobjekt so konstruieren, dass es die Elemente des arguments-Objekts durchläuft:

function values() {
   var i = 0, n = arguments.length;
   return {
      hasNext: function() {
         return i < n;
      },
      next: function() {
         if (i >= n) {
            throw new Error("end of iteration");
         }
         return arguments[i++]; // Falsche Argumente
      }
   };
}

Dieser Code funktioniert aber nicht, was sofort klar wird, wenn wir versuchen, unser Iteratorobjekt zu nutzen:

var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // undefined
it.next(); // undefined
it.next(); // undefined

Die Ursache des Problems ist die Tatsache, dass die neue Variable arguments implizit an den Rumpf der jeweiligen Funktionen gebunden wird. Wir sind an dem arguments-Objekt interessiert, das mit der values-Funktion zusammenhängt, aber die Methode next des Iterators enthält ihre eigene arguments-Variable. Wenn wir arguments[i++] zurückgeben, greifen wir daher nicht auf die Argumente von values zu, sondern auf ein Argument von it.next.

Die Lösung ist ganz unkompliziert: Erstellen Sie einfach eine neue lokale Variable im Gültigkeitsbereich des arguments-Objekts. Danach binden Sie sie an das Objekt, an dem wir interessiert sind, und sorgen Sie dafür, dass die verschachtelten Funktionen nur auf diese ausdrücklich benannte Variable verweisen:

function values() {
   var i = 0, n = arguments.length, a = arguments;
   return {
      hasNext: function() {
      return i < n;
    },
    next: function() {
       if (i >= n) {
          throw new Error("end of iteration");
       }
       return a[i++];
    }
  };
}
var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // 1
it.next(); // 4
it.next(); // 1

Thema 25 Extrahieren Sie Methoden mit festem Empfänger per bind

Da es keine Unterscheidung zwischen einer Methode und einer Eigenschaft mit einer Funktion als Wert gibt, ist es ganz leicht, eine Methode aus einem Objekt zu entnehmen und direkt als Callback an eine Funktion höherer Ordnung zu übergeben. Genauso leicht vergisst man dabei aber, dass der Empfänger der extrahierten Funktion nicht an das Objekt gebunden ist, aus dem sie entnommen wurde. Stellen Sie sich ein kleines Stringpufferobjekt vor, das Strings zur späteren Verkettung in einem Array speichert:

var buffer = {
   entries: [],
   add: function(s) {
      this.entries.push(s);
   },
   concat: function() {
      return this.entries.join("");
   }
};

Es sieht so aus, als könnten Sie ein Array aus Strings in den Puffer kopieren, indem Sie seine Methode add extrahieren und mithilfe der ES5-Methode forEach wiederholt für jedes Element des Quellarrays aufrufen:

var source = ["867", "-", "5309"];
source.forEach(buffer.add); // Fehler: entries ist undefined

Allerdings ist der Empfänger von buffer.add gar kein buffer. Der Empfänger einer Funktion wird durch die Art und Weise des Aufrufs bestimmt, wobei wir die Funktion hier gar nicht aufrufen, sondern nur an die Methode forEach übergeben, deren Implementierung sie an irgendeiner von uns nicht einsehbaren Stelle aufruft. Wie sich herausstellt, nutzt die Implementierung von forEach das globale Objekt als Standardempfänger. Da dieses Objekt aber keine Eigenschaft namens entries hat, ruft der Code einen Fehler hervor. Zum Glück bietet for-Each dem Aufrufer die Möglichkeit, ein optionales Argument bereitzustellen, mit dem der Empfänger des Callbacks festgelegt wird. Damit können wir den Beispielcode ganz einfach korrigieren:

var source = ["867", "-", "5309"];
source.forEach(buffer.add, buffer);
buffer.concat(); // "867-5309"

Nicht alle Funktionen höherer Ordnung sind aber so nett, ihren Clients die Möglichkeit zu bieten, einen Empfänger für ihre Callbacks anzugeben. Wie müssten wir vorgehen, wenn forEach das optionale Empfängerargument nicht akzeptieren würde? Eine gute Lösung besteht darin, eine lokale Funktion zu erstellen, die dafür sorgt, dass buffer.add mit der geeigneten Syntax als Methode aufgerufen wird:

var source = ["867", "-", "5309"];
source.forEach(function(s) {
   buffer.add(s);
});
buffer.concat(); // "867-5309"

Diese Version erstellt eine Wrapper-Funktion, die add ausdrücklich als Methode von buffer aufruft. Beachten Sie, dass diese Wrapper-Funktion nirgendwo auf this verweist. Wie auch immer sie aufgerufen wird – als Funktion, als Methode eines anderen Objekts oder mit call –, sie verschiebt ihr Argument immer in das Zielarray.

bind

Eine Version von einer Funktion zu erstellen, die den Empfänger an ein bestimmtes Objekt bindet, ist eine so häufig vorkommende Aufgabe, dass in der Bibliothek von ES5 sogar Unterstützung dafür integriert wurde. Funktionsobjekte weisen die Methode bind auf, die ein Empfängerobjekt annimmt und eine Wrapper-Funktion erstellt, mit der die ursprüngliche Funktion als Methode des Empfängers aufgerufen wird. Mit bind können wir unser Beispiel wie folgt vereinfachen:

var source = ["867", "-", "5309"];
source.forEach(buffer.add.bind(buffer));
buffer.concat(); // "867-5309"

Denken Sie daran, dass buffer.add.bind(buffer) nicht einfach die Funktion buffer.add bearbeitet, sondern eine neue Funktion erstellt. Diese neue Funktion verhält sich genauso wie die alte, wobei jedoch ihr Empfänger an buffer gebunden ist, während die alte Funktion unverändert bleibt. Mit anderen Worten:

buffer.add === buffer.add.bind(buffer); // false

Das ist ein sehr feiner, aber entscheidender Unterschied, denn dadurch können Sie bind gefahrlos selbst für Funktionen aufrufen, die auch in anderen Teilen des Programms verwendet werden. Besonders wichtig ist dies für Methoden mit demselben Prototypobjekt. Sie funktionieren auch dann noch korrekt, wenn sie über irgendeinen Abkömmling des Prototyps aufgerufen werden. (Mehr über Objekte und Prototypen erfahren Sie in Kapitel 4.)

Thema 26 Nutzen Sie bind beim Currying

Die Methode bind einer Funktion eignet sich noch für weitere Zwecke als nur für die Bindung von Methoden an Empfänger. Betrachten Sie als Beispiel eine einfache Funktion, die URL-Strings aus einzelnen Bestandteilen zusammenstellt:

function simpleURL(protocol, domain, path) {
  return protocol + "://" + domain + "/" + path;
}

Es kommt häufig vor, dass ein Programm absolute URLs aus websitespezifischen Pfadstrings zusammenstellen muss. Eine offensichtliche Möglichkeit dazu besteht darin, die ES5-Methode map für Arrays einzusetzen:

var urls = paths.map(function(path) {
   return simpleURL("http", siteDomain, path);
});

Beachten Sie, dass die anonyme Funktion in jeder Iteration von map denselben Protokoll- und denselben Domänenstring verwendet. Die ersten beiden Argumente von simpleURL sind für jede Iteration identisch, nur das dritte Argument wird benötigt. Wenn wir die Methode bind für simpleURL anwenden, können wir diese Funktion automatisch konstruieren:

var urls = paths.map(simpleURL.bind(null, "http", siteDomain));

Der Aufruf von simpleURL.bind erzeugt eine neue Funktion, die eine Delegierung an simpleURL vornimmt. Wie immer gibt das erste Argument von bind den Empfänger an. (Da simpleURL nicht auf this verweist, können wir hier jeden beliebigen Wert verwenden, wobei null und undefined üblich sind.) Die an simpleURL übergebenen Argumente werden dadurch konstruiert, dass die restlichen Argumente von simpleURL.bind mit jeglichen für die neue Funktion bereitgestellten Argumenten verkettet werden. Mit anderen Worten: Wenn das Ergebnis von simpleURL.bind nur mit dem Argument path aufgerufen wird, dann nimmt die Funktion eine Delegierung an simpleURL("http", siteDomain, path) vor.

Haskell Curry

Diese Technik, eine Funktion an eine Teilmenge ihrer Argumente zu binden, wird als Currying bezeichnet; Namensgeber ist der Logiker Haskell Curry, der sie in der Mathematik bekannt gemacht hat. Sie bildet eine kompakte Möglichkeit, um eine Delegierung von Funktionen mit weit weniger Standardcode zu implementieren als mit Wrapper-Funktionen.

Thema 27 Kapseln Sie Code mit Closures, nicht mit Strings

Funktionen bieten eine bequeme Möglichkeit, um Code in Form von Datenstrukturen zu speichern, die sich später ausführen lasen. Dadurch sind ausdrucksstarke Funktionen höherer Ordnung wie map und for-Each möglich. Außerdem bilden sie den Kern der asynchronen Vorgehensweise von JavaScript für die Ein-/Ausgabe (siehe Kapitel 7). Es ist jedoch auch möglich, Code in Form eines Strings darzustellen, der an eval übergeben wird. Programmierer stehen daher vor der Frage, ob sie Code als Funktion oder als String darstellen sollen.

Entscheiden Sie sich im Zweifelsfall für eine Funktion. Strings sind als Codedarstellungen weit weniger flexibel, und das aus einem sehr wichtigen Grund: Sie sind keine Closures.

Als String

Betrachten Sie als Beispiel eine einfache Funktion, die eine vom Benutzer angegebene Aktion mehrfach wiederholt:

function repeat(n, action) {
   for (var i = 0; i < n; i++) {
      eval(action);
   }
}

Im globalen Gültigkeitsbereich funktioniert dies recht gut, da alle Variablenverweise in dem String von eval als globale Variablen aufgefasst werden. Beispielsweise kann ein Skript, das die Ausführungsgeschwindigkeiten von Funktionen vergleicht, zur Speicherung der Zeitwerte durchaus auf die globalen Variablen start und end zurückgreifen:

var start = [], end = [], timings = [];
repeat(1000,
       "start.push(Date.now()); f(); end.push(Date.now())");
for (var i = 0, n = start.length; i < n; i++) {
   timings[i] = end[i] - start[i];
}

Das Skript ist aber instabil. Wenn wir den Code einfach in eine Funktion verlagern, sind start und end keine globalen Variablen mehr:

function benchmark() {
   var start = [], end = [], timings = [];
   repeat(1000,
      "start.push(Date.now()); f(); end.push(Date.now())");
   for (var i = 0, n = start.length; i < n; i++) {
      timings[i] = end[i] - start[i];
   }
   return timings;
}

Diese Funktion veranlasst repeat dazu, Verweise auf die globalen Variablen start und end auszuwerten. Bestenfalls fehlt eine dieser globalen Variablen, weshalb der Aufruf von benchmark den Fehler ReferenceError hervorruft. Wenn wir Pech haben, ruft der Code jedoch push für irgendwelche globalen Objekte auf, die zufällig an start und end gebunden sind, weshalb das Programm unvorhersehbar reagiert.

Als Funktion

Eine stabilere API nimmt statt des Strings eine Funktion entgegen:

function repeat(n, action) {
   for (var i = 0; i < n; i++) {
      action();
   }
}

Dadurch kann das Skript benchmark gefahrlos auf lokale Variablen in einer Closure verweisen, die es als wiederholten Callback übergibt:

function benchmark() {
   var start = [], end = [], timings = [];
   repeat(1000, function() {
      start.push(Date.now());
      f();
      end.push(Date.now());
   });
   for (var i = 0, n = start.length; i < n; i++) {
      timings[i] = end[i] - start[i];
   }
   return timings;
}

Ein weiteres Problem bei eval besteht darin, dass es für Hochleistungs-Engines gewöhnlich schwerer ist, Code zu optimieren, der in einem String steht, da der Compiler für diesen Vorgang nicht rechtzeitig auf den Quellcode zugreifen kann. Ein Funktionsausdruck kann zum gleichen Zeitpunkt kompiliert werden wie der Code, in dem er steht, weshalb er eher der Standardkompilierung unterliegt.

Thema 28 Verlassen Sie sich nicht auf die toString-Methode

JavaScript-Funktionen weisen ein besonderes Merkmal auf, nämlich die Möglichkeit, ihren Quellcode als String auszugeben:

(function(x) {
   return x + 1;
}).toString(); // "function (x) {\n return x + 1;\n}"

Die Reflektion des Quellcodes einer Funktion ist eine leistungsfähige Möglichkeit, und geschickte Hacker finden gelegentlich raffinierte Verwendungszwecke dafür. Die Methode toString einer Funktion ist jedoch starken Einschränkungen unterworfen.

toString ist nicht eindeutig.

Erstens stellt der ECMAScript-Standard keinerlei Ansprüche an den String, den die toString-Methode einer Funktion ausgibt, weshalb bei den einzelnen JavaScript-Engines jeweils ganz unterschiedliche Strings dabei herauskommen können – auch solche, die keinerlei Ähnlichkeit mit der Funktion haben.

In der Praxis versuchen JavaScript-Engines, eine getreue Wiedergabe des Quellcodes der Funktion bereitzustellen, sofern die Funktion in reinem JavaScript implementiert wurde. Ein Beispiel für eine Situation, in der dieser Ansatz schiefgeht, sind Funktionen, die mit den Bibliotheken der Hostumgebung erstellt wurden:

(function(x) {
   return x + 1;
}).bind(16).toString(); // "function (x) {\n [native code]\n}"

Da die Funktion bind in vielen Hostumgebungen in einer anderen Programmiersprache implementiert ist (gewöhnlich in C++), ergibt dies eine kompilierte Funktion ohne JavaScript-Code, der in der Umgebung bereitgestellt werden könnte.

Da der Standard den Browser-Engines eine unterschiedliche Ausgabe von toString erlaubt, kann es nur zu leicht vorkommen, dass ein Programm in einem JavaScript-System funktioniert und in einem anderen nicht. Die einzelnen JavaScript-Implementierungen weisen kleine Abweichungen auf (z.B. in der Leerzeichenformatierung), die ein Programm versagen lassen, wenn es gegenüber den Einzelheiten der Quell-strings von Funktionen zu empfindlich ist.

Lokale Variablen in Closures

Schließlich ist der von toString hervorgerufene Quellcode auch keine Darstellung der Closures, die die mit den inneren Variablenverweisen verbundenen Werte erhalten, wie das folgende Beispiel zeigt:

(function(x) {
   return function(y) {
     return x + y;
   }
})(42).toString(); // "function (y) {\n return x + y;\n}"

Der resultierende String enthält immer noch den Verweis auf die Variable x, obwohl die Funktion eine Closure ist, die x an 42 bindet.

Aufgrund dieser Einschränkungen kann man sich nicht darauf verlassen, dass toString den Funktionscode auf nutzbringende und zuverlässige Weise extrahiert, weshalb man im Allgemeinen darauf verzichten sollte. Für anspruchsvolle Anwendungen der Quellcodeextraktion von Funktionen sollten Sie sorgfältig gestaltete JavaScript-Parser und Verarbeitungsbibliotheken einsetzen. Im Zweifelsfall ist es am sichersten, JavaScript-Funktionen als Abstraktionen zu betrachten, die Sie nicht wieder konkretisieren sollten.

Thema 29 Vorsicht, wenn Sie den Call Stack inspizieren!

Viele JavaScript-Umgebungen haben früher Möglichkeiten zur Inspektion des Call Stack bereitgestellt, also der Kette von aktiven Funktionen, die zurzeit ausgeführt werden. (Mehr darüber erfahren Sie in Thema 64.)

arguments.callee

In einigen älteren Hostumgebungen hatte jedes arguments-Objekt die beiden zusätzlichen Eigenschaften arguments.callee (die sich auf die aufgerufene Funktion bezog) und arguments.caller (für die aufrufende Funktion). Erstere wird in vielen Umgebungen nach wie vor unterstützt, bietet aber so gut wie keinen Nutzen, außer dass sie anonymen Funktionen erlaubt, sich auf sich selbst zu beziehen:

var factorial = (function(n) {
   return (n <= 1) ? 1 : (n * arguments.callee(n - 1));
});

Aber auch diese Art von Rekursion ist nicht besonders brauchbar, da es für eine Funktion viel einfacher ist, sich anhand ihres Namens auf sich selbst zu beziehen:

function factorial(n) {
   return (n <= 1) ? 1 : (n * factorial(n - 1));
}

arguments.caller

Mit der Eigenschaft arguments.caller lässt sich schon mehr anfangen: Sie verweist auf die Funktion, die den Aufruf mit dem gegebenen arguments-Objekt durchgeführt hat. Aus Sicherheitsgründen wurde dieses Merkmal aber aus fast allen Umgebungen entfernt, weshalb Sie sich nicht darauf verlassen können. Viele JavaScript-Umgebungen stellen mit caller eine ähnliche Eigenschaft für Funktionsobjekte bereit, die zwar nicht dem Standard entspricht, aber weitverbreitet ist. Sie verweist auf den letzten Aufrufer der Funktion:

function revealCaller() {
   return revealCaller.caller;
}

function start() {
   return revealCaller();
}

start() === start; // true

Die Versuchung ist groß, diese Eigenschaft einzusetzen, um eine Stack-Spur zu extrahieren, also eine Datenstruktur, die eine Momentaufnahme des aktuellen Call Stack darstellt. Es scheint verführerisch einfach zu sein, damit eine Stack-Spur zu erstellen:

function getCallStack() {
   var stack = [];
   for (var f = getCallStack.caller; f; f = f.caller) {
      stack.push(f);
   }
   return stack;
}

Bei einfachen Call Stacks funktioniert getCallStack gut:

function f1() {
   return getCallStack();
}

function f2() {
   return f1();
}

var trace = f2();
trace; // [f1, f2]

getCallStack lässt sich jedoch sehr schnell durcheinander bringen. Wenn eine Funktion mehr als einmal im Call Stack erscheint, bleibt die Logik zur Stack-Inspektion in einer Schleife hängen!

function f(n) {
   return n === 0 ? getCallStack() : f(n - 1);
}
var trace = f(1); // Endlosschleife

Was ist hier schiefgegangen? Da sich die Funktion f rekursiv selbst aufruft, wird ihre Eigenschaft caller automatisch so aktualisiert, dass sie zurück auf f verweist. Die Schleife in getCallStack sucht also ständig nach f und hängt sich dabei auf. Selbst wenn wir solche zyklischen Bezüge erkennen könnten, würden wir keine Angaben darüber finden, von welcher Funktion f vor diesen Selbstaufrufen aufgerufen worden ist. Alle Informationen über den Rest des Call Stacks sind verloren gegangen.

All diese Möglichkeiten zur Stack-Inspektion sind nicht im Standard enthalten und daher nur eingeschränkt portierbar und anwendbar. Vor allem aber sind solche Aufrufe im Strict Mode der ES5-Implementierungem unzulässig. Jeder Versuch, auf die Eigenschaften caller oder callee von strengen Funktionen oder arguments-Objekten zuzugreifen, ruft einen Fehler hervor:

function f() {
   "use strict";
   return f.caller;
}
f(); // Fehler: Der Zugriff auf caller ist bei strengen Funktionen
     // nicht zulässig

Am besten ist es, ganz auf die Stack-Inspektion zu verzichten. Wenn Sie sich den Stack nur zum Debuggen genauer ansehen wollen, eignet sich dazu ein interaktiver Debugger1 besser.