JavaScript/Operatoren/Rest- oder Spread-Operator

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der Operator ... besitzt zwei grundsätzlich verschiedene Bedeutungen. Abhängig davon, in welchem Kontext er notiert wird, dient er entweder als Rest-Operator oder als Spread-Operator. Der erstgenannte Rest-Operator kann zum Beispiel zur Erstellung von Restparametern genutzt werden und mit dem Spread-Operator ist es möglich, die Elemente eines iterierbaren Objektes in ein Array oder eine Liste mit Argumenten einzufügen.


Rest Operator

Wird der Operator ... zusammen mit einem Bezeichner als letzter Eintrag der Parameterliste einer Funktion oder bei der Destrukturierung eines iterierbaren Objektes notiert, dann erzeugt er als Rest-Operator ein Array mit den beim Aufruf an die Funktion übergebenen Argumenten, die nicht zuvor an formale Parameter gebunden wurden, beziehungsweise ein Array mit denjenigen Werten, mit denen bei der Destrukturierung zuvor keine Variable oder Konstante initialisiert wurde. Er sammelt also die restlichen Werte ein.


Beispiel
function getRest (parameter, ...rest) {
  console.info(parameter);
  return rest;
}

const array = getRest(16, 32, 64); // 16

console.log(array); // [32, 64]


In diesem Beispiel wird zunächst eine Funktion mit einem gewöhnlichen Parameter deklariert, welche darüber hinaus die Anweisung enthält, den Wert dieses Parameters in die Konsole zu schreiben. Nach diesem Parameter ist ein sogenannter Restparameter notiert, also der Rest-Operator und danach ein frei gewählter Parametername. Der Wert mit dem der Restparameter initialisiert wird ist zugleich auch der Rückgabewert der Funktion. Bei dem folgenden Aufruf der Funktion werden nun drei Argumente übergeben, wobei der als erstes deklarierte Parameter der Funktion mit dem Wert des ersten Arguments initialisiert wird, sodass entsprechend dessen Wert in die Konsole geschrieben wird. Die Werte der anderen beiden Argumente werden in das vom Rest-Operator erzeugte Array eingefügt, welches von der Funktion zurückgegeben wird, wie die abschließende Ausgabe in der Konsole zeigt.


Beispiel
const array = [16, 32, 64];

let [first, ...rest] = array;

console.log(rest); // [32, 64]


Hier wird zunächst in Literal-Schreibweise ein Array mit drei Elementen erzeugt und einer Konstante als Wert zugewiesen. Das auf diese Weise gespeicherte Array wird dann im nächsten Schritt destrukturiert, wobei die Variable mit dem Bezeichner first mit dem Wert des ersten Elementes des Arrays initialisiert wird. Das zweite und letzte Zuweisungsziel innerhalb der Liste der Elemente für die Zuweisung (Assignment Element List) ist nun der Rest-Operator und der danach notierte Bezeichner rest. Analog zum Restparameter im letzten Beispiel wird hier ein Array mit den verbliebenen Werten erzeugt, das an die Variable rest gebunden wird, wie die Ausgabe in der Konsole bestätigt.

Erstellung von Restparametern

Es kommt nicht selten vor, dass eine Funktion eine unbestimmte Anzahl an Argumenten entgegennehmen soll, was den Programmierer dann vor die Frage stellt, wie die beim Aufruf tatsächlich übergebenen Argumente adressiert werden sollen. Denn die Deklaration von formalen Parametern stellt hierbei in der Regel keine praktikable Lösung dar.


Beispiel
function test (parameter) {
  console.log(Array.isArray(arguments));
  return arguments.length;
}

var count = test(2, 4, 8); // false

console.log(count); // 3


Bis zur Einführung des Rest-Operators bestand die einzige Lösung für dieses Problem darin, das eingebaute Objekt zu verwenden, welches innerhalb von Funktionen über den Bezeichner arguments referenziert werden kann. Dieses enthält grundsätzlich alle Werte, die beim Funktionsaufruf übergeben wurden. Allerdings ist die Verwendung von arguments selbst wiederum mit einigen Problemen behaftet. So werden in diesem Objekt auch diejenigen Werte gespeichert, welche bereits an Parameter gebunden wurden, weshalb unter Umständen die restlichen, nicht bereits an formale Parameter gebundenen Argumente von Hand separiert werden müssen. Darüber hinaus handelt es sich bei dem Objekt arguments nicht um ein echtes Array, wie die Überprüfung mit der Methode isArray in dem Beispiel oben zeigt. Dementsprechend können die verschiedenen von Array.prototype vererbten Methoden nicht direkt auf diesem Objekt ausgeführt werden.


Beispiel
function getRest (parameter) {
  return Array.prototype.slice.call(arguments, 1);
}

var array = getRest(128, 256, 512);

console.log(array); // [256, 512]


Um gegebenenfalls an formale Parameter gebundene Werte von den restlichen Argumenten zu trennen und um die Möglichkeit zu haben, mit den von Array.prototype vererbten Methoden direkt auf der Liste der Argumente zu operieren, musste der Inhalt des Objektes arguments also bislang zunächst in ein Array kopiert werden, was in der Regel durch den Aufruf der Arraymethode slice im Kontext von arguments bewerkstelligt wurde, etwa so wie in dem Beispiel oben. Zwar ließe sich die gezeigte Syntax noch abkürzen, indem man die Methode slice nicht direkt über Array.prototype referenziert, sondern zu diesem Zweck ein temporäres leeres Array erzeugt, aber umständlich ist der Vorgang allemal.


Hinweis:
Wenn eine Funktion mit formalen Parametern eine unbestimmte Anzahl weiterer Argumente entgegennehmen soll und es beabsichtigt ist, auf allen an die Funktion übergebenen Werten zu operieren, also auch auf den Werten, mit denen die formalen Parameter initialisiert wurden, dann bietet sich die Verwendung der Methode from des Konstruktors Array an, um den Inhalt von arguments in ein natives Array zu kopieren. Zu diesem Zweck die Methode slice zu verwenden ist grundsätzlich nicht mehr zu empfehlen.


Der in der sechsten Edition der Sprache eingeführte Rest-Operator bietet jedenfalls eine im Vergleich deutlich elegantere Schreibweise, um das Problem der Adressierung einer unbestimmten Anzahl an Argumenten zu lösen, denn mit ihm kann ein sogenannter Restparameter erstellt werden, der ein echtes Array erzeugt und an den gewählten Parameternamen bindet, in dem nur diejenigen an die Funktion übergebenen Werte gespeichert werden, die nicht bereits an formale Parameter gebunden wurden. Sofern exklusiv auf diesen Werten operiert werden soll, kann man sich den Umweg über das Objekt arguments also nunmehr komplett sparen.


Beispiel
function getArguments (...values) {
  console.log(Array.isArray(values));
  return values;
}

const array = getArguments(16, 32, 64); // true

console.log(array); // [16, 32, 64]


In diesem Beispiel wird zunächst eine Funktion deklariert, in deren Parameterliste nur der Rest-Operator und der frei gewählte Bezeichner values notiert ist. Die Funktion besitzt also lediglich einen Restparameter. Dieser wird nun beim Aufruf der Funktion mit einem Array initialisiert, welches die Werte der drei übergebenen Argumente enthält. Die Überprüfung mit der Methode isArray zeigt, dass es sich bei dem Wert von values tatsächlich um ein echtes Array handelt. Da für die Funktion keine anderen Parameter deklariert wurden, entspricht der Inhalt des an den Restparameter gebundenen Arrays also dem Inhalt des Objektes arguments. Das Kopieren der Werte, um auf der Liste der Argumente Arraymethoden auszuführen, ist hier also überflüssig.


Beispiel
function getRest (first, second, ...rest) {
  console.log(rest.length);
  return rest;
}

const array = getRest(4, 8, 16, 32); // 2

array.forEach(value => console.log(value)); // 16, 32


Für die Funktion, welche in diesem Beispiel deklariert wird, sind außer dem Restparameter noch zwei normale Parameter notiert. Diese werden nun eins zu eins mit den Werten der ersten beiden Argumente initialisiert, die beim Aufruf der Funktion übergeben wurden. Das an den Restparameter gebundene Array enthält nun anders als das Objekt arguments nicht mehr alle beim Aufruf übergebenen Werte, sondern nur noch diejenigen Werte, mit denen zuvor kein anderer Parameter initialisiert wurde. Die Trennung von formalen Parametern und sonstigen an die Funktion übergebenen Argumenten erfolgt hier also bereits im Kopf der Funktion und muss nicht umständlich selbst durchgeführt werden.


Hinweis:
Restparameter werden bei der Berechnung des Wertes der Eigenschaft length einer Funktion übrigens ebenso wie Parameter mit Standardwerten nicht berücksichtigt. Das heißt, bezogen auf die Funktion im letzten Beispiel würde die Eigenschaft length den Wert 2 zurückgeben, da die Parameterliste zwei formale Parameter enthält, für die keine Standardwerte angegeben wurden.


Werden beim Aufruf einer Funktion für die ein Restparameter deklariert wurde keine Argumente übergeben, oder wurden alle tatsächlichen Argumente an formale Parameter gebunden, dann wird durch den Rest-Operator dennoch ein Array erzeugt und mit dem gewählten Bezeichner verknüpft. In diesem Fall enthält das Array jedoch keine Elemente.


Beispiel
// syntax error
function fails (...rest, parameter) {
  console.log(parameter);
  return rest;
}


Zu beachten ist bei der Deklaration eines Restparameters natürlich, dass dieser in der Liste der Parameter der Funktion zwingend an letzter Stelle notiert werden muss. Wird nach dem Restparameter nämlich noch ein weiterer Parameter notiert, dann wird hierdurch ein Syntaxfehler erzeugt. Eine solche Notierung wäre darüber hinaus auch sinnlos, da alle verbliebenen Werte bereits in das vom Rest-Operator erzeugte Array eingefügt wurden und ein danach notierter Parameter entsprechend nur mit dem Wert undefined initialisiert werden könnte.


Beispiel
// syntax error
function fails (...) { }


Ebenfalls einen Syntaxfehler erzeugt das Weglassen des Bezeichners nach dem Rest-Operator, aber auch das versteht sich eigentlich von selbst, denn in diesem Fall könnte das erstellte Array im Körper der Funktion nicht referenziert werden. Es wäre also ziemlich nutzlos. Dem zur Folge muss nach dem Rest-Operator also immer ein valider Bezeichner notiert werden, wobei es im Übrigen aber kein Fehler ist, anders als in den Beispielen dieses Abschnitts zwischen dem Operator und dem Bezeichner Leerzeichen einzufügen.

Destrukturierung iterierbarer Objekte

Außer bei der Erstellung von Restparametern kann der Rest-Operator auch bei der Destrukturierung von iterierbaren Objekten verwendet werden, bei der er jedoch denselben Zweck erfüllt, nämlich Werte einzusammeln, für die zuvor keine Bindung erzeugt wurde. Wird der Rest-Operator also zusammen mit einem Bezeichner an letzter Stelle innerhalb der Liste der Elemente für die Zuweisung notiert, dann wird auch hierbei ein Array erzeugt, dessen Elemente wiederum die verbliebenen Werte sind, mit denen zuvor keine Konstante oder Variable initialisiert wurde.


Beispiel
const array = ['boolean', true, false];

const [type, ...values] = array;

console.log(values); // [true, false]


In der ersten Anweisung dieses Beispiels wird ein Array mit drei Elementen erzeugt und gespeichert. Da es sich bei Arrays um iterierbare Objekte handelt, ist es möglich sie auf die hier gezeigte Weise zu destrukturieren, was nun im nächsten Schritt geschieht, wobei die Konstante mit dem Bezeichner type mit dem Wert des erstens Arrayelements initialisiert wird. Der letzte Eintrag in der von eckigen Klammern umschlossenen Liste ist der Rest-Operator mit dem Bezeichner values und diese Notierung sorgt nun dafür, dass ein Array mit den beiden verbliebenen Werten des destrukturierten Arrays erzeugt wird. Mit diesem Array wird in der Folge die Konstante mit dem Namen values initialisiert, was durch die Ausgabe in der Konsole bestätigt wird.


Beispiel
function numbers ([type, ...entries]) {
  return type !== 'number' ? false : entries;
}

const set = new Set([
  'number', 3, 5, 7
]);

console.log(numbers(set)); // [3, 5, 7]


Die Destrukturierung von iterierbaren Objekten, zu denen unter anderem auch Sets gehören, ist darüber hinaus nicht auf die explizite Deklaration beziehungsweise Initialisierung von Variablen oder Konstanten beschränkt, sondern es ist ebenso möglich, wie in dem Beispiel oben ein Objekt zu destrukturieren, welches beim Funktionsaufruf als Argument übergeben und dann an einen Parameter gebunden wurde, und auch hier kann der Rest-Operator entsprechend dazu verwendet werden, die restlichen Werte einzusammeln. Bezogen auf das letzte Beispiel werden also implizit zwei Variablen mit den Bezeichnern type und entries erzeugt und letztere wird mit einem Array initialisiert, welches die restlichen Einträge des als Argument übergebenen Sets enthält.


Beispiel
const array = [11, 13, 17];

// syntax error
let [...entries, last] = array;


Nicht anders als bei der Erstellung von Restparametern ist aber auch bei der Destrukturierung iterierbarer Objekte darauf zu achten, dass der Rest-Operator immer als letzter Eintrag der Liste der Elemente für die Zuweisung notiert werden muss, da andernfalls ein Syntaxfehler geworfen wird. Das Gleiche gilt für den Fall, dass nach dem Operator kein valider Bezeichner notiert wurde.


Beispiel
const object = {first : 'alpha', last  : 'omega'};

// syntax error
const {first, ...rest} = object;


Schließlich bleibt zu erwähnen, dass der Rest-Operator nicht bei der Destrukturierung von Objekten verwendet werden kann, die nicht iterierbar sind, also kein Iterable Interface implementiert haben, so wie dies bei planen Objekten der Fall ist. Wird also ein solches Objekt wie in dem Beispiel oben destrukturiert und dabei der Rest-Operator notiert, dann wird hierdurch ein Syntaxfehler erzeugt, und es werden nicht etwa die Werte der verbliebenen Objekteigenschaften in einem Array hinterlegt.

Spread-Operator

Wird der Operator ... in einem Arrayliteral notiert oder in der Liste der Argumente beim Aufruf einer Funktion und ihm ein iterierbares Objekt übergeben, dann extrahiert er als Spread-Operator die Werte dieses Objektes und fügt sie als Elemente in das Array ein, beziehungsweise er wandelt die Werte in Argumente um, mit denen die Funktion dann aufgerufen wird.


Beispiel
function argumentsToArray ( ) {
  return [...arguments];
}

const array = argumentsToArray(8, 16, 32);

console.log(array); // [8, 16, 32]


In diesem Beispiel wird zunächst eine Funktion ohne formale Parameter deklariert, die lediglich eine Rückgabeanweisung enthält. Zurückgegeben werden soll dabei ein Array, welches in Form eines Literals notiert ist. In diesem Arrayliteral wird nun der Spread-Operator notiert und ihm eine Referenz auf das Objekt arguments übergeben, bei dem es sich um ein iterierbares Objekt handelt, welches alle Argumente enthält, mit denen die Funktion aufgerufen wurde. Diese Notierung sorgt nun dafür, dass alle Elemente des Objektes arguments, bei dem es sich selbst nicht um ein echtes Array handelt, in das von der Funktion zurückgegebene Array kopiert werden.


Hinweis:
Im Abschnitt über Restparameter wurde empfohlen, den Inhalt des Objektes arguments mit der Methode from des Konstruktors Array in ein natives Array zu kopieren. Dazu sei angemerkt, dass eine Notierung wie in dem Beispiel oben zu dieser Variante und im Gegensatz zur Verwendung der Methoden call und slice, prinzipiell eine gleichwertige Alternative darstellt. Es ist allerdings zu berücksichtigen, dass die Syntax der Methode from tendenziell besser lesbar ist und die Methode darüber hinaus die Möglichkeit bietet, durch die Übergabe einer Rückruffunktion direkt auf den extrahierten Werten zu operieren, sodass dieser Variante im Zweifel der Vorzug zu geben ist.


Der folgende Aufruf der zuvor deklarierten Funktion und die Ausgabe des Rückgabewertes in der Konsole zeigt jedenfalls, dass tatsächlich alle übergebenen Argumente, die zunächst in dem eingebauten Objekt arguments gespeichert waren, vom Spread-Operator in das erstellte Array kopiert wurden.


Beispiel
function test (first, second, third) {
  if (arguments.length) {
    console.log(first);
  }
}

const args = [32, 64, 128];

test(...args); // 32


In diesem Beispiel wird nun ebenfalls zunächst eine Funktion deklariert, welche im Gegensatz zu der Funktion im letzten Beispiel jedoch über drei formale Parameter verfügt. Sie enthält die Anweisung, den Wert des ersten Parameters in die Konsole zu schreiben, wenn der Funktion bei ihrem Aufruf Argumente übergeben wurden, was über die Eigenschaft length des Objektes arguments ermittelt wird. Danach wird ein Array mit drei Elementen erzeugt, welches der Konstante mit dem Bezeichner args als Wert zugewiesen wird. Schließlich wird die zuvor deklarierte Funktion aufgerufen, wobei in der von runden Klammern umschlossenen Liste der Argumente der Spread-Operator notiert und ihm eine Referenz auf das Array übergeben wird. Der Operator wandelt nun die Elemente des Arrays in Argumente für die Funktion um, sodass die formalen Parameter der Funktion mit den Werten initialisiert werden, die in dem übergebenen Array hinterlegt sind. Entsprechend wird der Wert des erstens Elementes des Arrays in die Konsole geschrieben.

Verwendung in Arrayliteralen

Wenn der Spread-Operator innerhalb eines Arrayliterals notiert und ihm ein iterierbares Objekt übergeben wird, dann iteriert er über dieses Objekt und fügt die dabei ausgegebenen Werte an der Stelle in das Array ein, an der er notiert wurde, was im Gegensatz zum Rest-Operator allerdings nicht zwingend an letzter Stelle sein muss.


Beispiel
const object = {0 : 'first', length : 1};

// type error
const array = [...object];


Voraussetzung hierfür ist allerdings die Iterierbarkeit des übergebenen Objektes, das heißt, bloße Array-ähnlichkeit, also das Vorhandensein über einen ganzzahligen Index adressierbarer Eigenschaften und einer Eigenschaft length, genügt hier anders als etwa bei der Methode Array.from nicht. Hat das übergebene Objekt, wie es zum Beispiel bei planen Objekten standardmäßig der Fall ist, nicht selbst oder über ein Objekt in der Prototypenkette eine entsprechende Schnittstelle implementiert, dann wird bei der Übergabe an den Spread-Operator ein Typfehler geworfen.


Beispiel
const source = [8, 16, 32];

const target = [4, ...source, 64];

console.log(target); // [4, 8, 16, 32, 64]


Jedenfalls handelt es sich bei nativen Arrays um iterierbare Objekte, weshalb dem Spread-Operator ein solches übergeben werden kann. In dem Beispiel oben wird also zunächst ein Array mit drei Elementen erzeugt. Im nächsten Schritt wird dann in Literalschreibweise ein weiteres Array erstellt und darin der Spread-Operator notiert, dem eine Referenz auf das zuvor erstellte Array übergeben wird. Der Operator iteriert nun über die Elemente des übergebenen Arrays und fügt sie an der entsprechenden Stelle in das zweite Array ein. Dass die Operation geglückt ist, zeigt die abschließende Ausgabe in der Konsole.


Beispiel
const source = [8, 16, 32];

const target = [ ].concat(4, source, 64);

console.log(target); // [4, 8, 16, 32, 64]


Wollte man ohne den Spread-Operator das gleiche Ergebnis wie im letzten Beispiel erzielen, dann wäre hierfür die Arraymethode concat zu verwenden. Allerdings ist diese Syntax weniger elegant und sie geht darüber hinaus mit dem Nachteil einher, dass die Methode concat nur Werte aus Arrays extrahiert, nicht jedoch Werte anderer iterierbarer Objekte, wie beispielsweise Maps, Sets oder dem Objekt arguments. Die Kompatibilität vorausgesetzt, ist es in der Regel also zu empfehlen, bei der Konkatenation von Arrays nicht die Methode concat sondern den Spread-Operator zu verwenden.


Beispiel
const planets = new Set(['Mars', 'Jupiter', 'Saturn']);

const names = [...planets, 'Neptune'];

console.log(names.length); // 4


Wie gesehen, muss dem Spread-Operator nicht unbedingt ein Array übergeben werden. So wird dem Operator in dem Beispiel oben statt eines Arrays ein Set übergeben und auch hier werden die in diesem Objekt enthaltenen Einträge extrahiert und in das Array eingefügt, in dem der Operator notiert wurde. Die Ausgabe der Eigenschaft length des Arrays zeigt, dass nunmehr vier Elemente in dem Array enthalten sind, die Werte also erfolgreich eingefügt wurden.


Beispiel
const planets = new Map([
  ['Earth', 'blue'],
  ['Mars', 'red']
]);

console.log([
  'Venus', ...planets.keys( ) // ['Venus', 'Earth', 'Mars']
]);


In diesem Beispiel wird zunächst eine Map mit zwei Einträgen erstellt. Beim Aufruf von console.log wird nun ein Array als Argument übergeben, in dem der Spread-Operator notiert ist. Dem Operator übergeben wird dabei der Rückgabewert der Methode keys, also ein Iteratorobjekt, das bei der Iteration die Schlüssel der Einträge der Map ausgibt. Da dieses Objekt iterierbar ist, werden entsprechend die Schlüssel der beiden in der Map hinterlegten Einträge in das Array eingefügt, das der Methode log übergeben wurde.


Hinweis:
Die Mapinstanz selbst ist ebenfalls iterierbar und hätte dementsprechend dem Spread-Operator auch direkt übergeben werden können. Allerdings besteht das durch die Methode entries definierte Standardverhalten von Maps bei der Iteration darin, die Einträge in Form von Arrays mit zwei Elementen auszugeben, also in der gleichen Form, in der die Einträge bei der Erzeugung der Map an den Konstruktor übergeben wurden. Will man also wie hier nur über die Schlüssel der Map iterieren, dann ist der Iterator der Methode keys zu verwenden. Soll hingegen nur über die Werte iteriert werden, wäre die Methode values aufzurufen, die einen entsprechenden Iterator zurückgibt.


Dem Spread-Operator können darüber hinaus aber nicht nur eingebaute iterierbare Objekte wie Arrays, Maps oder Sets übergeben werden, denn dies würde zumindest teilweise dem Sinn und Zweck der Standardisierung der Protokolls für die Iteration widersprechen. Das heißt, es kann natürlich auch ein selbsterstelltes iterierbares Objekt übergeben werden, sofern alle für die Iteration erforderlichen Schnittstellen definiert sind.


Beispiel
const planets = {
  names : ['Mercury', 'Venus'],
  * [Symbol.iterator] ( ) {
    yield * this.names;
  }
};

const names = [...planets, 'Earth'];

console.log(names); // ['Mercury', 'Venus', 'Earth']


In diesem Beispiel wird in Literalschreibweise ein gewöhnliches Objekt erstellt, das standardmäßig nicht iterierbar ist. Für dieses Objekt wird dann eine Generatormethode als Iterable Interface definiert, welche bei der Iteration die Werte eines in dem Objekt gespeicherten Arrays ausgibt. Das auf diese Weise iterierbar gemachte Objekt wird dann im nächsten Schritt dem in einem Arrayliteral notierten Spread-Operator übergeben, und auch hier werden die Werte extrahiert und in das Array eingefügt, wie die Ausgabe in der Konsole zeigt. Die in dem Beispiel gezeigte Syntax stellt im Übrigen nur eine und auch nicht unbedingt die sinnvollste Variante dar, wie die entsprechenden Schnittstellen implementiert werden können, jedoch würde eine genauere Beschäftigung mit dem Thema Iteration den Rahmen dieses Artikels sprengen.

Erzeugung von Argumenten

Wird der Operator ... innerhalb der runden Klammern bei einem Funktionsaufruf notiert und ihm ein iterierbares Objekt übergeben, dann wandelt er die bei der Iteration ausgegebenen Werte in Argumente um, mit denen dann die gegebenenfalls deklarierten formalen Parameter der Funktion initialisiert werden.


Beispiel
function getParameter (first, last) {
  return first;
}

const values = [128, 256];

console.log(getParameter(...values)); // 128


In diesem Beispiel wird als erstes eine Funktion mit zwei formalen Parametern definiert, welche die Anweisung enthält, den Wert des ersten Parameters zurückzugeben. Als nächstes wird ein Array mit zwei Elementen erzeugt und gespeichert. Schließlich wird die zuvor deklarierte Funktion aufgerufen und dabei anstelle einzelner Argumente der Spread-Operator notiert, dem eine Referenz auf das Array übergeben wird. Der Operator wandelt nun die in dem Array enthaltenen Elemente in Argumente für den Funktionsaufruf um, sodass die beiden formalen Parameter der Funktion in der Folge mit diesen beiden Werten initialisiert werden. Der Wert des ersten Parameters wird zurückgegeben und in die Konsole geschrieben und es zeigt sich, dass er dem Wert des ersten Elementes des Arrays entspricht.

Aufruf einer Funktion oder Methode

Es kommt ziemlich oft vor, dass die Werte, mit denen eine Funktion oder Methode aufgerufen werden soll, in einer Datenstruktur wie einem Array vorliegen. Erwartet die Funktion die Werte allerdings nicht in einer solchen Form, sondern als einzelne Parameter, dann müssen die Werte extrahiert und einzeln übergeben werden. Die bis zur Einführung des Spread-Operators einzige praktikable Lösung für dieses Problem war die Verwendung der Methode apply, die von Function.prototype an alle Funktionsobjekte vererbt wird.


Beispiel
var values = [11, 13, 17];

function test (alfa, bravo, charlie) {
  [alfa, bravo, charlie].forEach(function (value) {
    console.log(value);
  });
}

test.apply(null, values); // 11, 13, 17


In dem Beispiel oben wird eine Funktion, die drei formale Parameter besitzt, unter Verwendung der Methode apply aufgerufen. Da es sich bei der Funktion nicht um eine Methode handelt, wird apply als erstes Argument, mit dem die Funktionsvariable this initialisiert wird, der Wert null übergeben. Als zweites Argument wird eine Referenz auf ein zuvor erstelltes Array mit drei Elementen übergeben und die in dem Array enthaltenen Werte werden nun von der Methode apply in Argumente umgewandelt, sodass die formalen Parameter der Funktion damit befüllt werden können. Die Ausgabe der Parameterwerte in der Konsole zeigt, dass die Methode apply funktioniert.


Hinweis:
Die Funktionsmethode apply konnte früher nur solche Werte extrahieren und in Argumente umwandeln, die in einem nativen Array gespeichert waren. Seit der fünften Edition der Sprache ist es aber auch möglich, Array-ähnliche Objekte zu übergeben, sodass zum Beispiel auch das Objekt arguments verarbeitet werden kann. Allerdings werden intern nur Eigenschaften mit einem ganzzahligen Index als Eigenschaftsname sequenziell gelesen und deren Werte in die Liste der Argumente eingefügt, es wird also nicht im sprachspezifischen Sinne über das Objekt iteriert. Dem zur Folge können iterierbare Objekte wie zum Beispiel Maps oder Sets nicht als Argument an die Methode apply übergeben werden.


Das gleiche Resultat wie im letzten Beispiel kann nunmehr auch unter Verwendung des Spread-Operators erzielt werden, was im Allgemeinen deutlich eleganter ist, als die Funktion oder Methode mittels apply aufzurufen, von der hier fehlenden Möglichkeit einmal abgesehen, die jeweilige Funktion in einem bestimmten Kontext aufzurufen.


Beispiel
const values = [23, 29, 31];

function test (alfa, bravo, charlie) {
  [alfa, bravo, charlie].forEach(value => console.log(value));
}

test(...values); // 23, 29, 31


In diesem Beispiel wird also ebenfalls ein Array mit den Werten erzeugt, mit denen die formalen Parameter der deklarierten Funktion initialisiert werden sollen. Beim Aufruf der Funktion wird nun einfach der Spread-Operator innerhalb der runden Klammern nach dem Funktionsbezeichner notiert und ihm eine Referenz auf das Array übergeben. Wie die Ausgabe in der Konsole zeigt, wurden auch hier die Werte aus dem Array extrahiert und als Argumente an die Parameter der Funktion gebunden.

Beachten Sie: Funktionen erhalten ihre Argumente über den Stack, eine spezielle Speicherstruktur, die für die Verwaltung von Funktionsaufrufen zuständig ist. Dieser kann nicht beliebig groß werden, deshalb begrenzt die JavaScript-Umgebung die Anzahl der Argumenten für eine Funktionen „auf mehrere Zehntausend“[1]. Der konkrete Wert ist von der JavaScript-Engine abhängig. „Spreizen“ Sie also nur dann Arrays in eine Argumentliste, wenn sie wissen, dass die Anzahl der Arrayelemente überschaubar ist. Andernfalls müssen Sie versuchen, Datengruppen zu bilden oder den Code generell umzustrukturieren.
Beispiel
const part = [37, 41, 43];

const object = {
  method ( ) {
    Array.from(arguments).forEach(value => console.log(value));
  }
};

object.method(31, ...part, 47); // 31, 37, 41, 43, 47


Im Gegensatz zur Methode apply können bei der Verwendung des Spread-Operators zusätzlich zu den Werten die in dem iterierbaren Objekt enthalten sind, das dem Operator übergeben wird, noch weitere Argumente notiert werden, wie das Beispiel oben zeigt. Es ist dabei auch unerheblich, an welcher Stelle in der Liste der Argumente der Spread-Operator notiert wird, er fügt analog zum Verhalten bei der Notierung in einem Arrayliteral die Werte des iterierbaren Objektes einfach an der entsprechenden Stelle ein.


Beispiel
const colors = new Set(['blue', 'red']);

function planets (earth, mars) {
  [earth, mars].forEach(value => console.log(value));
}

planets(...colors); // blue, red


Wie in diesem Beispiel zu sehen ist, können mit dem Spread-Operator auch Werte aus einem Objekt extrahiert und in Argumente umgewandelt werden, das nicht Array-ähnlich ist, was mit der Methode apply nicht möglich wäre. So sind die Werte für den Funktionsaufruf hier in der internen Liste eines Sets gespeichert und nicht als Objekteigenschaften. Da der Spread-Operator jedoch das Protokoll für die Iteration beherrscht, werden auch hier die Werte extrahiert und einzeln an die Funktion übergeben.


Beispiel
const numbers = new Set([17, 3, 11, 5, 23]);

let max = Math.max(...numbers);

console.info(max); // 23


Im Übrigen ist die Verwendung des Spread-Operators natürlich nicht auf selbstdefinierte Funktionen beschränkt, sondern er kann auch beim Aufruf von eingebauten Funktionen verwendet werden, wie zum Beispiel der Methode max des Standardobjektes Math. Diese Methode ermittelt aus einer beliebigen Anzahl numerischer Werte den jeweils größten Wert und gibt ihn zurück, wobei die Methode max die zu prüfenden Werte als einzelne Argumente erwartet. Der Spread-Operator kann nun auch hier dazu verwendet werden, um elegant eine größere Anzahl an Werten aus einer Datenstruktur wie einem Array oder einem Set zu extrahieren und sie der Methode max zu übergeben.

Konstruktorenaufruf

Sind die Werte die als Argumente verwendet werden sollen in einem Array oder Array-ähnlichen Objekt hinterlegt und soll die Funktion oder Methode normal aufgerufen werden, dann kann das besagte Objekt ohne weiteres an die Methode apply übergeben werden, wie im letzten Abschnitt gezeigt wurde. Problematisch ist hingegen der Aufruf einer Funktion als Konstruktor, wenn man den Spread-Operator nicht verwenden kann, denn die Methode apply kann für sich genommen nur für normale Funktionsaufrufe verwendet werden, nicht jedoch für Konstruktorenaufrufe.


Beispiel
var args = ['number', 42];

function Constructor (property, value) {
  'use strict';
  this[property] = value;
}

var instance = new (Function.prototype.bind.apply(
  Constructor, [null].concat(args)
));

console.log(instance.number); // 42


Will man also einen Konstruktor aufrufen und dabei die Argumente in Form eines Arrays übergeben, dann genügt es nicht, lediglich die Methode apply aufzurufen, sondern es ist etwas mehr Arbeit nötig. So kann wie in dem Beispiel oben mit der Methode bind eine an den Konstruktor gebundene Funktion erzeugt werden, wobei bind selbst mittels apply aufgerufen wird. Da hierbei jedoch auch ein Wert für den Kontext übergeben werden muss, ist dieser in das Array für die Übergabe an die Methode apply einzufügen, wofür hier die Arraymethode concat verwendet wird. Alles in allem also ein sehr umständliches Aufrufmuster.


Beispiel
const args = new Set(['number', 42]);

function Constructor (property, value) {
  if (!new.target) {
    throw new TypeError('Constructor called without new');
  }
  this[property] = value;
}

const instance = new Constructor(...args);

console.log(instance.number); // 42


Wesentlich eleganter lässt sich dies mit dem Spread-Operator bewerkstelligen, der mit dem Aufruf ansich nichts zu tun hat und deswegen natürlich auch beim Aufruf einer Funktion als Konstruktor verwendet werden kann. Darüber hinaus können selbstverständlich auch hier alle Objekte an den Operator übergeben werden, welche über ein Iterable Interface verfügen und es gibt keine Beschränkung auf Arrays, wenn es um die Speicherung der Werte für die Argumente der Funktion, beziehungsweise des Konstruktors geht.


Beispiel
class Parent {
  constructor ( ) {
    [this.first, this.last] = arguments;
  }
}

class Child extends Parent {
  constructor ( ) {
    super(...arguments);
    this.type = this.constructor.name;
  }
}

const instance = new Child('alpha', 'omega');

console.log(instance.last); // omega


Der Spread-Operator kann schließlich auch beim Aufruf einer Basisklasse über das Schlüsselwort super verwendet werden, um Argumente, die beim Aufruf einer abgeleiteten Klasse übergeben wurden, an den Konstruktor der Basisklasse weiterzureichen. Dabei kann entweder wie hier über das Objekt arguments iteriert werden, oder über einen Restparameter, den man natürlich in der Liste der Parameter der Konstruktormethode hätte notieren können.


Beispiel
const args = ['key', 'value'];

function Constructor (key, value) {
  if (!new.target) {
    throw new TypeError('Constructor called without new');
  }
  this[key] = value;
}

const instance = Reflect.construct(Constructor, args);

console.log(instance.key); // value


Nicht unerwähnt bleiben soll in diesem Zusammenhang allerdings, dass es seit der sechsten Edition der Sprache neben dem Spread-Operator noch eine weitere Alternative zur Syntax mit bind und apply für den Aufruf einer Funktion als Konstruktor gibt, wenn die Argumente in einem Array oder Array-ähnlichen Objekt hinterlegt sind und sie dem Konstruktor einzeln übergeben werden sollen. Denn in diesem Fall kann auch die Methode construct des Standardobjektes Reflect verwendet werden. Hierbei ist allerdings zu beachten, dass die Methode construct ebenso wie die Methode apply nur Arrays oder Array-ähnliche Objekte verarbeiten kann, nicht jedoch andere iterierbare Objekte. Sind die zu übergebenden Werte also beispielsweise in einer Map oder in einem Set hinterlegt, muss der Spread-Operator verwendet werden, um die Werte zu extrahieren und sie in die Liste der Argumente einzufügen.

Kompatibilität

Da der Operator ... erst seit der sechsten Edition des Standards ein Teil der Sprache ist, besteht die Möglichkeit, dass diese Syntax zum Zeitpunkt der Lektüre dieses Artikels noch nicht von allen für ein aktuelles Projekt relevanten Browsern und anderen Ausführungsumgebungen unterstützt wird. Daher sei dringend angeraten, vor der Verwendung dieses Operators zunächst zu prüfen, inwieweit dieser auch tatsächlich unterstützt wird. Dies kann zum Beispiel durch einen Blick auf die Tabelle zur Kompatibilität von kangax geschehen.

Weblinks

Einzelnachweise

  1. MDN: Function.prototype.apply: Using apply() and built-in functions