Orlok: spread und rest

Beitrag lesen

Hallo 1unitedpower

Man sieht auch häufig die Verwenundung des spread-Operators, um Iterierbares in Arrays zu konvertieren:

const elements = [...node.querySelectorAll('.gallery figcaption')]

Nur eine weitere Möglichkeit.

Dann habe ich auch noch eine. :-)

const [...elements] = node.querySelectorAll('.gallery figcaption')

Hier wird die iterierbare NodeList destrukturiert und die Elemente mittels Rest-Syntax an den Bezeichner der Konstante gebunden, wobei ebenfalls ein Array erzeugt wird.


Für diejenigen, die es interessiert, schauen wir uns zunächst mal die Syntax des ersten Ausdrucks etwas genauer an.

const elements = [...node.querySelectorAll('selectors')]

Das könnte man in etwa wiefolgt ableiten, wobei wir aus Gründen der besseren Lesbarkeit einige (viele) Ableitungsschritte auslassen:

<lexical-declaration>

=> <let-or-const> <binding-list>

=> const <binding-list>

=> const <lexical-binding>

=> const <binding-identifier> <initializer>

=> const <identifier> <initializer>

=> const elements <initializer>

=> const elements = <assignment-expression>

=> const elements = <primary-expression>

=> const elements = <array-literal>

=> const elements = [ <element-list> ]

=> const elements = [ <spread-element> ]

=> const elements = [... <assignment-expression> ]

=> const elements = [... <call-expression> ]

=> const elements = [... <member-expression> <arguments> ]

=> const elements = [... <identifier> . <identifier-name> <arguments> ]

=> const elements = [...node. <identifier-name> <arguments> ]

=> const elements = [...node.querySelectorAll( <argument-list> )]

=> const elements = [...node.querySelectorAll( <string-literal> )]

=> const elements = [...node.querySelectorAll('selectors')]

Von besonderem Interesse ist hier natürlich der folgende Ableitungsschritt, der zeigt, dass nach dem spread-Operator aus syntaktischer Sicht im Wesentlichen wirklich irgendein Ausdruck stehen kann. Die besonderen Anforderungen ergeben sich hier aus der Semantik.

=> const elements = [... <assignment-expression> ]

Der Ausdruck nach dem spread-Operator sollte zu einer Referenz auf ein Objekt aufgelöst werden, welches das Iterable Interface implementiert, das also iterierbar ist. Dazu gehört nicht viel, nur das Vorhandensein einer Methode mit einem bestimmten Eigenschaftsnamen, die wenn man sie aufruft, einen sogenannten Iterator zurückgibt.

Der Name der Methode muss das standardmäßig über die Objekteigenschaft Symbol.iterator referenzierbare Symbol @@iterator sein. Das ist eins von mehreren Symbolen, die auf dem eingebauten Funktionsobjekt Symbol hinterlegt sind und die es erlauben, den Ablauf von internen Algorithmen zu beeinflussen, wie eben bei der Iteration von Objekten.

typeof [][Symbol.iterator] // function

Das Codebeispiel oben zeigt, dass für Arrays eine entsprechende Methode definiert ist.

In bestimmten Kontexten, wie etwa bei einer Schleife mit for und of oder eben bei der Akkumulation von Arrays mittels spread-Syntax, wird diese Methode aufgerufen und es wird erwartet, dass sie ein Objekt zurückgibt, welches das Iterator Interface implementiert. Dieser Iterator enthält gewissermaßen die Information, wie über das jeweilige Objekt zu iterieren ist, also welche Werte in welcher Reihenfolge auszugeben sind.

Die Minimalanforderung an den Iterator ist, dass er über eine Methode namens next verfügt, die beim Aufruf ein Objekt mit zwei Eigenschaften zurückgibt, nämlich zum einen die Eigenschaft value, welche den Wert enthält, der im aktuellen Iterationsschritt ausgegeben werden soll, und zum anderen die Eigenschaft done, die einen booleschen Wert zurückgibt und die dazu dient mitzuteilen, wann die Iteration beendet werden soll, beispielsweise, weil alle Werte aus dem iterierbaren Objekt ausgegeben wurden.

const nodeList = [element1, element2, element3],

      nodeListIterator = nodeList[Symbol.iterator]()


const elements = [
    nodeListIterator.next().value, // element1
    nodeListIterator.next().value, // element2
    nodeListIterator.next().value  // element3
]


elements // [element1, element2, element3]

Das Beispiel oben zeigt also, was im Prinzip bei der Auswertung des hier betrachteten Ausdrucks passiert: Auf dem Objekt, das über den Ausdruck nach ... referenziert worden ist, wird die Methode Symbol.iterator aufgerufen und auf dem zurückgegebenen Iterator wird solange next aufgerufen bis alle Werte ausgegeben wurden. Die so gewonnenen Werte wiederum werden dazu verwendet, das neu erstellte Array zu initialisieren.

Schauen wir uns angefangen mit der Syntax nun den zweiten Ausdruck an:

const [...elements] = node.querySelectorAll('selectors')

Dieser Ausdruck lässt sich wiefolgt ableiten, wobei wir uns wieder die meisten Ableitungsschritte sparen, da sie nichts zur Sache tun:

<lexical-declaration>

=> <let-or-const> <binding-list>

=> const <binding-list>

=> const <lexical-binding>

=> const <binding-pattern> <initializer>

=> const <array-binding-pattern> <initializer>

=> const [ <binding-rest-element> ] <initializer>

=> const [... <binding-identifier> ] <initializer>

=> const [... <identifier> ] <initializer>

=> const [...elements] = <assignment-expression>

=> const [...elements] = <call-expression>

=> const [...elements] = <member-expression> <arguments>

=> const [...elements] = <identifier> . <identifier-name> <arguments>

=> const [...elements] = node. <identifier-name> <arguments>

=> const [...elements] = node.querySelectorAll( <argument-list> )

=> const [...elements] = node.querySelectorAll( <string-literal> )

=> const [...elements] = node.querySelectorAll('selectors')

An dieser Stelle interessieren wir uns besonders für den folgenden, aus Gründen der besseren Anschauung zusammengefassten Ableitungsschritt:

=> const [... <binding-identifier> ] = <assignment-expression>

Hierbei handelt es sich um ein Muster zur Destrukturierung von iterierbaren Objekten. Während auf der rechten Seite syntaktisch wieder irgendein Ausdruck stehen kann, muss auf der linken Seite innerhalb der Klammern und nach dem Operator ... ein valider Bezeichner stehen, der als Zuweisungsziel geeignet ist. Das kann wie im vorliegenden Fall der Bezeichner einer Konstante sein, aber auch eine Variable.

Werden in einem solchen Zuweisungsausdruck ein oder mehrere Bezeichner, durch Komma getrennt, in eckige Klammern gefasst, dann wird so wie oben beschrieben über das Objekt auf der rechten Seite iteriert und die dabei erhaltenen Werte werden einer nach dem anderen an die innerhalb der Klammern notierten Bezeichner gebunden.

Die in der Produktion <binding-rest-element> zusammengefasste Syntax stellt dabei einen Sonderfall da. Analog zur Notation von Restparametern von Funktionen führt die Notation des Operators ... vor einem Bezeichner dazu, dass die aus dem destrukturierten Objekt entnommenen Werte, welche nicht zuvor bereits an einen anderen Bezeichner in der Liste gebunden wurden, in ein neu erstelltes Array eingefügt werden, das dann an den nach dem Operator notierten Bezeichner gebunden wird.

Da in dem betrachteten Ausdruck vor ...elements keine Bezeichner notiert wurden, wird entsprechend der gesamte Inhalt der NodeList in das Rest-Array kopiert.

Viele Grüße,

Orlok