FOR und WHILE und JavaScript

Inzwischen ertappe ich mich immer häufiger dabei, dass ich for- und while-Schleifen meide, wenn ich in JavaScript programmiere; auch beim Refactoring tendiere ich dazu herkömmliche Schleifen zu ersetzen. Stattdessen verwende ich Funktionen höherer Ordnung, wie each, map, times und reduce für die es inzwischen für die meisten Browser eine native Implementierung gibt. Da das jedoch keineswegs sicher ist, greife ich auf die famose underscore-Bibliothek zurück, die zusätzlich weitere Feinheiten eines funktionalen toolbelts bereit hält.

Es ist absolut nachvollziehbar zu fragen, warum es sinnvoll sein sollte nicht auf die altgedienten Schleifen zurückzugreifen. Dafür habe ich im Folgenden einige – wie ich finde – gut Gründe zusammengetragen.

Es geht um Daten

Genauer geht es in der Regel um die Transformation von Daten: Ein Array oder ein Objekt soll aggregiert werden; oder es soll aus einem Array ein anderes gemacht werden; oder für jedes Key/Value-Paar eines Objekts soll eine bestimmte Operation ausgeführt werden.

Eine For-Schleife (aber genauso eine While-Schleife) macht nicht deutlich, was mit den Daten geschieht. Ein Beispiel:

var result = [],
    arr = [1, 2, 3, 4, 5];
for(var i = 0; i < arr.length; i++) {
    result.push( arr[i] * 2 );
}

Obwohl es sich um ein sehr einfaches Beispiel handelt, muss man trotzdem wenigstens einen kleinen Blick auf den Code und in die For-Schleife werfen, um zu verstehen, was geschieht. Wie viel schneller erkennt man den eigentlichen Zweck des Codes – aus einem schon vorhandenen Array ein neues zu machen, dessen Werte verdoppelt sind – hier:

var result = _.map(arr, function(number) {
    return number * 2;
});

Die spezielleren Funktionen höherer Ordnung erfüllen den Zweck, der ihnen den jeweiligen Namen gibt und erlauben es also viel schneller zu erkennen, was vor sich geht. Das ist insbesondere dann wertvoll, wenn man im Team programmiert oder seinen eigenen Code auch noch nach Wochen (oder Monaten) verstehen möchte.

Daneben gibt es aber noch ein weiteres Problem. Was ist mit dem folgenden Code-Fragment nicht in Ordnung?

for(var i = 0; i < arr.length; i++) {
    // do something...
}

Was sollte mich die Länge des Arrays interessieren? Ich möchte Daten transformieren, mich interessiert aber kein Stück wie viele Einzel-Daten ich habe. Doch geht damit ein weiteres Problem einher: Wenn das Array undefined oder null ist, kommt es zu einem hässlichen Fehler, weil diese Werte über kein Attribut length verfügen. In der Regel soll aber einfach nur nichts passieren, wenn keine Daten da sind. Die underscore-Funktionen arbeiten wie man es erwartet und schützen damit vor Ärger.

Weniger Variablen, klare Referenzen

Nicht nur interessiert nicht die Länge des Arrays, dessen Daten manipuliert werden sollen; in der Regel ist auch die Lauf-Variable nicht weiter von Interesse und kann getrost eingespart werden; selbstverständlich ist es auch weiterhin möglich auf den aktuellen Index zuzugreifen, sollte dies erforderlich sein. Man kann jedoch auf weitere Variablen verzichten, wie bspw. das im Beispiel oben definierte result. Während das Ergebnis einer Schleife nicht einfach in ein return geschrieben kann (nur mittelbar, mit einer zusätzlichen Variable) ist das mit einer Funktion problemlos möglich.

Weniger Variablen bedeuten mehr Übersichtlichkeit und höhere Verständlichkeit. Das heißt natürlich nicht, das Variablen nichts tolles sind und man auf sie verzichten sollte, wenn es sinnvoll ist. Aber wenn man auf sie verzichten kann, weil man sie halt gerade nicht braucht…

Ein weiteres unendliches Übel in Javascript:

var arr = [1, 2, 3];
var arr2 = arr;

arr2.pop();

console.log(arr);
// |> [1, 2]

Das nicht Werte, sondern Referenzen gesichert werden, führte bei mir häufig zu Problemen und Bugs, die ich nur schwer nachvollziehen konnte (inzwischen ist es besser geworden). Underscore bietet die clone-Funktion um Arrays nicht nach Referenz, sondern nach Werten zu kopieren; daneben sorgen map, reduce, etc. dafür, dass das Array, mit dem gearbeitet wird, nicht angefasst wird (sofern man nicht merkwürdige Dinge macht, wie in der anonymen Funktion push oder pop auf das Array anzuwenden), d. h. es bleibt in der gleichen Form erhalten, wie es war. Die Rückgabe ist wiederum unabhängig und besitzt keine Referenz auf das Ausgangsobjekt. Selbstverständlich braucht man keine funktionalen Konstrukte, um dieses Verhalten auch in normalen Schleifen sicher zu stellen, aber sie erleichtern einem das Leben und machen deutlich(er), was vor sich geht.

Nachvollziehbarer scope

Der scope ist in JavaScript nicht nach dem Block, sondern nach der Funktion beschränkt. Dies ist in anderen Sprachen, die eine C-ähnliche Syntax verwenden anders und – seien wir ehrlich – verwirrend. Dazu ein weiteres Beispiel:

for(var i = 0; i < arr.length; i++) {
    var s = "something";
    // do something else ...
}
console.log(s);
// |> "something"

Eigentlich würde man erwarten, dass die Variable s außerhalb der For-Schleife keine Gültigkeit mehr besitzt.

Dieser Verwirrung entgeht man durch die Verwendung einer anonymen Funktion, die den Part übernimmt, den sonst das Innere der Schleife hätte: Die Variablen sind sauber gekapselt, nichts dringt nach außen, was nicht nach außerhalb der Funktion sichtbar sein soll.

Fazit

Die Nutzung von Funktionen höherer Ordnung anstatt von For-Schleifen erhöht die Verständlichkeit des Codes, spart unsinnige Variablen ein und bewahrt vor schwer nachvollziehbaren Fehlern.

Einen kleinen Haken hat die Angelegenheit dann aber dennoch: Je nach Implementierung sind map, each, reduce und Konsorten (deutlich) langsamer. In der Regel hält sich der Einbußen an Geschwindigkeit aber in vernünftigen Grenzen und sollte nicht den anderen Vorzügen geopfert werden; zumal sich map, each, … effizient selbst implementieren lassen – mit Hilfe der For-Schleife.