Wenn ich Programmierern, die bisher mit Webentwicklung eher wenig am Hut hatten, JavaScript nahebringe, fange ich gerne mit den Bad Parts, den WTFs und den Wats an. Dann haben wir den unangenehmen Teil schnell hinter uns, ein paar Vorurteile werden bestätigt und Witze kann man damit auch schön reißen. Dass JS auch ein paar sehr feine Eigenschaften hat, kommt dann nach und nach von selbst zum Vorschein. Allerdings schickt sich ECMAScript 6 an, dieses Vorgehen in Zukunft zu erschweren, denn dort steht natürlich die Beseitigung (oder zumindest Abschwächung) der Bad Parts ganz oben auf der Agenda und eine dieser Maßnahmen zur WTF-Reduzierung sind Features rund um Funktionsparameter  da ist der Aufräumbedarf nämlich besonders hoch.

Fehlende Features und das Arguments-Problem

In JavaScript ist in Sachen „Bedienkomfort und Funktionsparameter“ viel Luft nach oben. So sieht in ECMAScript 5 der einzige Weg, Vorgabewerte für Funktionsparameter zu definieren, so aus:

var fn = function(x){
  if(typeof x === 'undefined'){
    x = 42;
  }
}

Das funktioniert einigermaßen, ist aber alles andere als komfortabel. Das nächste Problem zeigt sich, wenn man mit einer nach oben offenen Anzahl an Parametern zu arbeiten versucht. Innerhalb von Funktionen findet sich in JavaScript immer eine Variable namens arguments, die eine Liste der die Funktion beim Aufruf übergebenen Werte enthält und eigentlich mehr nach einem Feature als nach einem Problem aussieht:

var fn = function(){
  console.log(arguments);
};
fn('Hallo', 42)

// In der Konsole erscheint: ["Hallo", 42]

Die Sache hat aber einen Haken: arguments ist kein Array, sondern nur ein array-ähnliches Objekt. Spürbar wird der Unterschied vor allem darin, dass array-ähnlichen Objekten all die nützlichen Methoden fehlen, die Arrays so bieten: slice(), map(), forEach() und so weiter. Möchte man diese nutzen, so muss man arguments in ein echtes Array verwandeln, indem man es mit Array.prototype.slice() bearbeitet:

var fn = function(){
  var args = [].slice.call(arguments);
  return Array.isArray(args); // > true
};

Das ist zwar nur ein Einzeiler und für den geübten JavaScript-Entwickler nichts besonderes, aber trotzdem ist es schon recht lästig, dass man diese Umwandlung manuell vornehmen muss. Möchte man einen Schritt weiter gehen und nur einen Teil der Funktionsargumente als Array erhalten und den Rest weiterhin als normale Parameter verwenden, wird es endgültig unbequem:

var fn = function(a, b){
  var rest = (arguments.length >= 3) ? [].slice.call(arguments, 2) : [];
  console.log(rest);
};

fn(23, 42);             // > []
fn(23, 42, 1337, 9001); // > [1337, 9001]

Umgekehrt ist ist es auch nicht viel besser: hat man ein Array von Parametern für eine Funktion, so kommt man nicht um Function.prototype.apply() herum. Während das für sich genommen noch halbwegs unschlimm ist (abgesehen von der erforderlichen manuellen this-Festlegung), kann man, wenn man apply() benutzt, zusätzliche Werte ausschließlich durch Umbau des Arrays in die Funktion geben:

function fn(){
  console.log(arguments);
}

// Diese Args haben wir...
var args = [23, 42, 1337, 9001];

// ... aber vorher muss noch das hier dazu
args.unshift(2.718, 3.141);

fn.apply(undefined, args);
// > [2.718, 3.141, 23, 42, 1337, 9001] 

Eine Syntax für den Use Case „Funktionsaufruf mit A, B und allem was in diesem Array ist“ gibt es nicht, obwohl das ja nun wirklich keine abwegige Idee ist.

Die gute Nachricht ist, dass ECMAScript 6 all diese Probleme in Angriff nimmt. Es gibt eine Syntax für Funktionsparameter-Vorgabewerte und die neuen Rest- und Spread-Operatoren schicken arguments und Array-Gebastel in Rente. Wohlgemerkt: nur in Rente! Denn komplett abschaffen lassen sich die Bad Parts nicht …

Rest statt Arguments

Oberflächlich betrachtet wäre es ein schöner Fortschritt, wenn ECMAScript 6 arguments reparieren und in ein richtiges Array verwandeln würde. Das ist allerdings nicht möglich, da es da draußen im Web viel Code gibt, der fest davon ausgeht, dass arguments eben kein Array ist – und niemand möchte, dass plötzlich Scripts auf Webseiten nach einen Browserupdate einfach nicht mehr funktionieren. Also braucht es eine andere, arguments ergänzende Lösung, die in ES6 in Form von Rest-Parametern (ECMAScript-Wiki, MDN) daherkommt.

Rest-Parameter sind spezielle letzte Parameter in einer Funktionsdefinition, die mehrere an die Funktion übergebene Werte einfangen und in einem echten Array zur Verfügung stellen. Sie werden in der Funktionsdefinition mit drei vorangestellten Punkten markiert, tragen aber innerhalb der Funktion einen ganz normalen Variablennamen:

// Funktion mit zwei normalen Parametern
// Alle weiteren Parameter landen im Array "rest"
function fn(a, b, ...rest){
  return rest;
}

var rest = fn(23, 42, 1337, 9001); // > [1337, 9001]
Array.isArray(rest);               // > true

Für an CoffeeScript gewohnte Entwickler ist anzumerken, dass es nach aktuellem Stand nicht möglich sein wird, Rest-Parameter für etwas anderes als die letzten Parameter einer Funktion zu verwenden, so wie hier:

# Klappt, nur in CoffeeScript, nicht in ES6
fn = (x, rest..., y) -> rest
fn(23, 42, 1337, 9001) # > [42, 1337]

Die Vorteile von ...rest gegenüber arguments liegen auf der Hand: es ist ein echtes Array mit eingebauten Slice für die jeweils letzten an eine Funktion übergebenen Daten. Das arguments-Objekt wird aus Gründen der Abwärtskompatibilität bis ans Ende aller Tage ein Bestandteil von JavaScript bleiben, aber benutzen wird man es wesentlich seltener. Auch für Fälle, in denen man alle Funktionsparameter in einer Liste haben möchte, ...rest dank der vielen nützlichen Array-Methode vorzuziehen:

function add(...args){
  return args.reduce(function(a, b){
    return a + b;
  }, 0);
}

add(23, 42, 1337, 9001); // > 10402

Das heißt also im Endeffekt: arguments wird von ECMAScript 6 in den wohlverdienten Ruhestand geschickt.

Spread als Apply-Ergänzung

Möglicherweise ebenfalls aus CoffeeScript bekannt sind Spreads (ECMAScript-Wiki, MDN) mit denen sich eine Funktion mit einem Array von Parametern aufrufen lässt:

function sum(...args){
  return args.reduce(function(prev, curr){
    return prev + curr;
  }, 0);
}

var zahlen = [23, 42, 1337, 9001];
var summe = sum(...zahlen); // > 10402

Spreads vereinfachen nicht nur hier und da den Code, sondern ermöglichen auch Dinge, die mit ECMAScript 5 schichtweg nicht nur mit ausgesprochen mühsamen Tricks machbar waren. In ES5 gibt es Function.prototype.apply(), was verwendet werden kann um eine Funktion mit einer Liste von Werten als Arguments aufzurufen:

var summe = add.apply(null, [23, 42, 1337, 9001]);

Der Haken an apply() ist, dass man zwingend ein Objekt angeben muss, dass von der aufgerufenen Funktion als this verwendet werden soll. Das ist aber nicht immer möglich, z.B. bei Constructorfunktionen. Diese zeichnen sich ja gerade dadurch aus, dass sie ihr this-Objekt selbst erstellen und können daher nur mit new, nicht aber unter Zuhilfenahme apply() aufgerufen werden können. So weigert sich beispielsweise der Date-Constructor ohne new ein Objekt zu produzieren und spuckt stattdessen einen String aus:

var dateParams = [ 2014, 6 ];
new Date(dateParams[0], dateParams[1])); // > Objekt
Date.apply(null, dateParams));           // > String

Dabei ist Date noch ein relativ harmloses Beispiel – andere Funktionen würden mit apply() überhaupt nicht funktionieren und nicht mal ein falsches Ergebnis liefern. Mit Spreads löst sich der ganze Problemkomplex in Wohlgefallen auf, da es keinen Grund gibt, sie nicht mit Constructorfunktionen zu kombinieren:

var dateParams = [ 2014, 6 ];
new Date(dateParams[0], dateParams[1]); // > Objekt
new Date(...dateParams);                // > Objekt

Außer bei Funktionsaufrufen lassen sich Spreads auch für das Zusammenfügen von Arrays verwenden und ergänzen an dieser Stelle Array.prototype.concat():

var zahlen1 = [23, 42];
var zahlen2 = [1337, 9001, ...zahlen1]; // > [1337, 9001, 23, 42]

Es spricht auch nichts dagegen, die Spread-Syntax mehrfach in einem Funktionsaufruf oder einer Array-Erstellung zu verwenden:

var foo = [42, 1337];
var numbers = [23, ...foo, ...[9001]]; // > [23, 42, 1337, 9001]
sum(...[3.141], ...numbers);           // > 10406.141

Spreads sind einfach eine schöne kleine Ergänzung zur JavaScript-Syntax, die an vielen Stellen das vergleichsweise komplizierte apply() oder andere Hacks überflüssig macht.

Vorgabewerte für Parameter

Ein Feature, das fast jede Programmiersprache schon lange hat, landet mit ECMAScript 6 endlich auch in JavaScript:

function add(a, b = 42){
  return a + b;
}

add(23, 1337); // > 1360
add(23);       // > 65

Die Vorgabewerte werden erst zugewiesen, wenn die Funktion aufgerufen wird, was z.B. Funktionsaufrufe an der Stelle von Werten erlaubt. Im folgenden Beispiel wird für jeden add()-Aufruf auch getDefault() neu aufgerufen, was in unterschiedlichen Ausgabewerten resultiert:

function getDefault(){
  return (Math.random() > 0.5) ? 42 : 23;
};

function add(a, b = getDefault()){
  return a + b;
}

add(23); // > Manchmal 46, manchmal 65

Ein Vorgabewert wird immer dann von der Funktion benutzt, wenn der tatsächlich übergebene Wert undefined ist. Das hat zur Folge, dass man einen Vorgabewert auch durch ein explizites undefined nicht überschreiben kann:

function add(a, b = 42){
  return a + b;
}

add(23, undefined); // > 65

Es zeigt sich: die neue Syntax ist nichts weiter als ein 1:1-Ersatz des althergebrachten manuellen Wenn- Undefined-dann-X-Tests für Parameter, aber einfach wesentlich schöner und bequemer. Und das Beste ist: obwohl noch kaum ein Browser Vorgabewerte für Parameter unterstützt, kann man sie, wie auch Rest und Spread, bereits heute flächendeckend einsetzen.

Unterstützung und Ausblick

Der Firefox ist aktuell der alleinige Champion im Bereich Rest, Spread und Defaults. Rest- und Default-Parameter funktionieren seit Version 15, seit Nummer 15 auch Spreads für Arrays und seit 27 für Funktionen. Beim Rest der Browserwelt sieht es noch finster aus, wie uns die allmächtige Kompatibilitätstabelle wissen lässt. Allerdings werden alle drei neuen Features vom Traceur-Compiler perfekt unterstützt, so dass man sie im Prinzip ab sofort zum Einsatzbringen kann, vorausgesetzt der zusätzliche Kompilierschritt ist nicht zu abschreckend.