Auf unserer Tour durch die neuen Features von ES5 wird es Zeit, den letzten großen Themenkomplex in Angriff zu nehmen: Arrays. Arrays in JavaScript sind seltsame Zeitgenossen. Sie sind kein eigener Datentyp, sondern spezielle Objekte, die dem Programmierer viele Möglichkeiten bieten, sich in den eigenen Fuß zu schießen (Stichwort Array-Constructor, length-Eigenschaft). Auch in ES5 ändert sich daran nicht viel, denn anstelle größerer Umbauten es gibt „nur“ ein paar neue Funktionen, die das Arbeiten mit Arrays erleichtern.

Array oder nicht?

Dass der typeof-Operator mit schlafwandlerischer Sicherheit jedes Array als object identifiziert, dürfte einer der Gründe dafür sein, dass JavaScript nicht ernstgenommen wird. Dabei liegt typeof nicht falsch – JavaScript-Arrays sind nun mal Objekte. Das ändert natürlich nichts daran, dass typeof an dieser Stelle nicht sonderlich hilfreich ist. ES5 hilft, indem es die Funktion Array.isArray() einführt, die genau das macht, was man von ihr erwartet:

var foo = [];
typeof foo;         // "object"
Array.isArray(foo); // true

Warum „repariert“ man nicht einfach typeof? Das hat zwei Gründe: erstens hat, wie erwähnt, typeof nicht unrecht wenn es Arrays als Objekte identifiziert, zweitens ist das Web voll mit Scripts die sich auf das alte Verhalten von typeof verlassen. Würden das alle Browser von heute auf morgen ändern, wären die Auswirkungen so verheerend, dass kein Weg an einer neuen Funktion vorbeiführt. Wenn man bedenkt, wie kompliziert die Identifizierung von Arrays ist, sollte man einfach froh sein, dass es Array.isArray() gibt.

ForEach, Filter und Map

ForEach-Schleifen sollte jedem JavaScript-Programmierer aus seinem liebsten Framework bekannt sein und werden in der bekannten Form in ES5 fest eingeführt. Das Array wird einmal durchlaufen und eine Callback-Funktion wird der Reihe nach auf die Array-Elemente angewendet:

// Drei Alerts für drei Zahlen
var zahlen = [6, 9, 12];
zahlen.forEach(function(zahl){
    alert(zahl);
});

Während forEach() nichts zurückgibt und nur mit den einzelnen Array-Elementen arbeitet, produzieren map() und filter() neue Arrays. Dabei filtert mittels filter() eines Callbacks ein Array; gibt der Callback true zurück, wird das Element in das neue Array gepackt, bei false nicht:

// Sortiert ungerade Zahlen aus
var zahlen = [6, 9, 12];
var gerade_zahlen = zahlen.filter(function(zahl){
    if(zahl % 2 == 0){
        return true;
    }
    else {
        return false;
    }
});

Der dritte Kandidat im Bunde, map(), wendet einen Callback auf alle Array-Elemente an. Die vom Callback zurückgegebenen Werte bilden dann ein neues Array:

// Verdoppelt die Zahlen im Array
var zahlen = [6, 9, 12];
var verdoppelte_zahlen = zahlen.map(function(zahl){
    return zahl * 2;
});

Vorsicht Falle: Die Callbacks von forEach(), filter() und map() bekommen drei Parameter übergeben! Neben dem akuellen Array-Element wird auch der aktuelle Index sowie das Array an sich übergeben. Das kann nützlich sein, kann aber zur Falle werden:

["6", "9", "12"].map(parseInt);

Hier sollen die Strings im Array in Integer verwandelt werden; man würde erwarten, dass man ein Array mit dem Inhalt [6, 9, 12] erhält. Das tatsächliche Ergebnis ist aber [6, NaN, 1]. Wie das sein kann? Ganz einfach: Wenn parseInt() ein zweites Argument übergeben bekommt, behandelt es diesen als Basis und produziert entsprechende Ergebnisse. Das ist zwar logisch, aber nicht gerade intuitiv – also immer schön aufpassen.

Alle drei neuen Funktionen nehmen neben dem Callback auch noch einen zweiten Parameter an, der bestimmt, welches Objekt im Callback für this verwendet wird. Wird hier nichts oder null angegeben, ist this das globale Objekt (es sei denn man befindet sich im Strict Mode, wo dies bekanntlich nicht mehr möglich ist).

Every und Some

Die beiden Array-Methoden every() und some() wenden einen Prüf-Callback auf die Elemente eines Arrays an. Der Callback prüft, ob die Elemente einer gewissen Bedingung entsprechen und gibt true oder false zurück. Der Unterschied zwischen every() und some(): ersteres gibt true zurück, wenn alle Array-Elemente den Test bestanden haben, letzteres auch dann, wenn nur ein einziges Element die Bedingungen erfüllt.

var arr = [2, 4, 6, 7, 11];

// False - es sind nicht ALLE Elemente gerade Zahlen
arr.every(function(element){
    return (element % 2 === 0);
});

// True - Einige Elemente SIND gerade Zahlen
arr.some(function(element){
    return (element % 2 === 0);
});

Wie auch bei forEach(), filter() und map() bekommt der Callback drei Argumente übergeben – neben dem zu prüfenden Element auch seinen Index und das gesamte Array. Auch das this des Callbacks kann über ein zweites Argument für every() und some() bestimmt werden.

Reduce und ReduceRight

Wenn es darum geht, ein Array auf einen einzigen Wert einzudampfen, sind reduce() und reduceRight() die Mittel der Wahl. Beide gehen ein Array Element für Element durch (leere Elemente werden übersprungen) und wenden einen Callback auf die Elemente an. Dem Callback wird dabei einerseits der Wert des aktuellen Elements übergeben, andererseits auf der Rückgabewert des vorherigen Callback-Ausrufs. So kann man zum Beispiel bequem die Zahlen in einem Array aufsummieren:

var arr = [1, 2, 3];

// Summiert alle Elemente des Arrays auf (Resultat: 6)
arr.reduce(function(prev, curr){
    return prev + curr;
});

Der Unterschied zwischen reduce() und reduceRight() ist, dass ersteres das Array von links nach rechts durchgeht, letzteres von rechts nach links:

var arr = ["A", "B", "C"];

// Ergebnis: ABC
arr.reduce(function(prev, curr){
    return prev + curr;
});

// Ergebnis: CBA
arr.reduceRight(function(prev, curr){
    return prev + curr;
});

Der Callback erhält wie üblich neben dem vorherigen Rückgabewert und dem aktuellen Elemente auch den Index des aktuellen Elements und das gesamte Array. Über ein zweites Argument von reduce() bzw. reduceRight() kann man den Startwert für den ersten Callback-Aufruf festlegen:

var arr = [1, 2, 3];

// Summiert alle Elemente des Arrays und den Startwert auf (Resultat: 10)
arr.reduce(function(prev, curr){
    return prev + curr;
}, 4);

IndexOf und LastIndexOf

Schon gewusst, dass indexOf() ein Teil von ES5 und damit eine so eine Art Neuheit ist? Zusammen mit lastIndexOf() dient es bei der Positionsbestimmung eines Elements in einem Arrray, wobei indexOf() den ersten Index und lastIndexOf() den letzten Index zurückgibt.

var arr = ["a", "b", "c", "a", "d"];
arr.indexOf("a");     // 0
arr.lastIndexOf("a"); // 3

Neben dem Element, nach dem in dem Array gesucht werden soll, kann auch ein Startindex für die Suche angegeben werden. Dabei sucht indexOf() von diesem Startindex aus vorwärts und lastIndexOf() rückwärts.

var arr = ["a", "b", "c", "a", "d"];
arr.indexOf("a", 1);     // 3
arr.lastIndexOf("a", 2); // 0

Wie geht es weiter?

Nachdem, wie zu erwarten war, uns der Blick auf die allmächtige Kompatibilitätstabelle freudig stimmt (außer in ältere IE funktioniert der Array-Teil von ES5 überall) bleibt die Frage wie es denn jetzt weitergeht. In Sachen ES5 gibt es nicht mehr viel zu berichten, denn alles wirklich neue haben wir bereits abgearbeitet. Dinge wie JSON und String.prototype.trim sind zwar streng genommen auch ES5, sind aber auch bereits allgemein bekannt und von den Browsern gut unterstützt.

In den folgenden Teilen der Serie werden wir daher noch weiter in die Zukunft vorstoßen. ECMAScript 5 ist ja streng genommen schon ein altes Eisen – immerhin datieren die Spezifikationen vom Dezember 2009. Zeit also, sich mit dem wirklich Neuen zu befassen, das zur Zeit noch den Arbeitstitel „ECMAScript Harmony“ trägt. Das wenige davon, das man tatsächlich schon anfassen kann, ist nur punktuell in Browsern implementiert und es ist nicht gesagt, dass es irgendwann in seiner heutigen Form auch Standard wird, aber zum experimentieren reicht allemal. Themen wie Traceur, Node.js und CoffeeScript werden wir sicher auch mal anschneiden können.