In diesem Kapitel betrachten wir Destrukturierung aus einem anderen Blickwinkel: als rekursiven Musterabgleich-Algorithmus.
Der Algorithmus wird uns ein besseres Verständnis von Standardwerten vermitteln. Das wird nützlich sein, wenn wir am Ende versuchen herauszufinden, wie sich die folgenden beiden Funktionen unterscheiden.
Eine Destrukturierungszuweisung sieht so aus:
Wir möchten pattern verwenden, um Daten aus value zu extrahieren.
Wir werden uns nun einen Algorithmus zur Durchführung dieser Art von Zuweisung ansehen. Dieser Algorithmus ist in der funktionalen Programmierung als Musterabgleich (kurz: Abgleich) bekannt. Er spezifiziert den Operator ← („abgleichen mit“), der ein pattern gegen einen value abgleicht und dabei Variablen zuweist.
Wir werden nur die Destrukturierungszuweisung untersuchen, aber Destrukturierung von Variablendeklarationen und Destrukturierung von Parameterdefinitionen funktionieren ähnlich. Wir werden uns auch nicht mit fortgeschrittenen Funktionen befassen: berechnete Schlüssel, Kurzformen für Eigenschaftswerte und Objekt-Eigenschaften und Array-Elemente als Zuweisungsziele liegen außerhalb des Rahmens dieses Kapitels.
Die Spezifikation für den Abgleichoperator besteht aus deklarativen Regeln, die in die Strukturen beider Operanden absteigen. Die deklarative Notation mag gewöhnungsbedürftig sein, macht die Spezifikation aber prägnanter.
Die in diesem Kapitel verwendeten deklarativen Regeln arbeiten mit Eingaben und erzeugen das Ergebnis des Algorithmus durch Nebeneffekte. Dies ist eine solche Regel (die wir später wiedersehen werden):
(2c) {key: «pattern», «properties»} ← obj
Diese Regel hat die folgenden Teile:
In Regel (2c) bedeutet der Kopf, dass diese Regel angewendet werden kann, wenn ein Objektmuster mit mindestens einer Eigenschaft (deren Schlüssel key ist) und null oder mehr verbleibenden Eigenschaften vorhanden ist. Die Auswirkung dieser Regel ist, dass die Ausführung mit dem Muster des Eigenschaftswerts, das gegen obj.key abgeglichen wird, und den verbleibenden Eigenschaften, die gegen obj abgeglichen werden, fortgesetzt wird.
Betrachten wir noch eine Regel aus diesem Kapitel:
(2e) {} ← obj (keine Eigenschaften mehr übrig)
In Regel (2e) bedeutet der Kopf, dass diese Regel ausgeführt wird, wenn das leere Objektmuster {} gegen einen Wert obj abgeglichen wird. Der Körper bedeutet, dass wir in diesem Fall fertig sind.
Zusammen bilden Regel (2c) und Regel (2e) eine deklarative Schleife, die über die Eigenschaften des Musters auf der linken Seite des Pfeils iteriert.
Der vollständige Algorithmus wird über eine Sequenz von deklarativen Regeln spezifiziert. Nehmen wir an, wir möchten den folgenden Abgleich-Ausdruck auswerten:
{first: f, last: l} ← obj
Um eine Sequenz von Regeln anzuwenden, gehen wir sie von oben nach unten durch und führen die erste anwendbare Regel aus. Wenn sich im Körper dieser Regel ein Abgleich-Ausdruck befindet, werden die Regeln erneut angewendet. Und so weiter.
Manchmal enthält der Kopf eine Bedingung, die ebenfalls bestimmt, ob eine Regel anwendbar ist – zum Beispiel:
(3a) [«elements»] ← non_iterable
if (!isIterable(non_iterable))
Ein Muster ist entweder:
x{«properties»}[«elements»]Die nächsten drei Abschnitte spezifizieren Regeln für den Umgang mit diesen drei Fällen in Abgleich-Ausdrücken.
x ← value (einschließlich undefined und null)(2a) {«properties»} ← undefined (ungültiger Wert)
(2b) {«properties»} ← null (ungültiger Wert)
(2c) {key: «pattern», «properties»} ← obj
(2d) {key: «pattern» = default_value, «properties»} ← obj
(2e) {} ← obj (keine Eigenschaften mehr übrig)
Die Regeln 2a und 2b behandeln ungültige Werte. Die Regeln 2c–2e durchlaufen die Eigenschaften des Musters. In Regel 2d sehen wir, dass ein Standardwert eine Alternative bietet, gegen die abgeglichen werden kann, wenn in obj keine übereinstimmende Eigenschaft vorhanden ist.
Array-Muster und Iterable. Der Algorithmus für Array-Destrukturierung beginnt mit einem Array-Muster und einem Iterable:
(3a) [«elements»] ← non_iterable (ungültiger Wert)
if (!isIterable(non_iterable))
(3b) [«elements»] ← iterable
if (isIterable(iterable))
Hilfsfunktion
function isIterable(value) {
return (value !== null
&& typeof value === 'object'
&& typeof value[Symbol.iterator] === 'function');
}Array-Elemente und Iterator. Der Algorithmus fährt fort mit:
Dies sind die Regeln:
(3c) «pattern», «elements» ← iterator
(3d) «pattern» = default_value, «elements» ← iterator
(3e) , «elements» ← iterator (Loch, Elision)
(3f) ...«pattern» ← iterator (immer der letzte Teil!)
(3g) ← iterator (keine Elemente mehr übrig)
Hilfsfunktion
function getNext(iterator) {
const {done,value} = iterator.next();
return (done ? undefined : value);
}Ein abgeschlossener Iterator ähnelt fehlenden Eigenschaften in Objekten.
Interessante Konsequenz der Regeln des Algorithmus: Wir können mit leeren Objekt- und Array-Mustern destrukturieren.
Gegeben ein leeres Objektmuster {}: Wenn der zu destrukturierende Wert weder undefined noch null ist, passiert nichts. Andernfalls wird ein TypeError ausgelöst.
const {} = 123; // OK, neither undefined nor null
assert.throws(
() => {
const {} = null;
},
/^TypeError: Cannot destructure 'null' as it is null.$/)Gegeben ein leeres Array-Muster []: Wenn der zu destrukturierende Wert iterierbar ist, passiert nichts. Andernfalls wird ein TypeError ausgelöst.
const [] = 'abc'; // OK, iterable
assert.throws(
() => {
const [] = 123; // not iterable
},
/^TypeError: 123 is not iterable$/)Mit anderen Worten: Leere Destrukturierungsmuster erzwingen, dass Werte bestimmte Eigenschaften haben, haben aber keine anderen Auswirkungen.
In JavaScript werden benannte Parameter über Objekte simuliert: Der Aufrufer verwendet ein Objektliteral und der Aufgerufene verwendet Destrukturierung. Diese Simulation wird ausführlich in „JavaScript für ungeduldige Programmierer“ erläutert. Der folgende Code zeigt ein Beispiel: Die Funktion move1() hat zwei benannte Parameter, x und y.
function move1({x=0, y=0} = {}) { // (A)
return [x, y];
}
assert.deepEqual(
move1({x: 3, y: 8}), [3, 8]);
assert.deepEqual(
move1({x: 3}), [3, 0]);
assert.deepEqual(
move1({}), [0, 0]);
assert.deepEqual(
move1(), [0, 0]);Es gibt drei Standardwerte in Zeile A.
x und y wegzulassen.move1() ohne Parameter aufzurufen (wie in der letzten Zeile).Aber warum sollten wir die Parameter wie im vorherigen Code-Snippet definieren? Warum nicht wie folgt?
Um zu sehen, warum move1() korrekt ist, werden wir beide Funktionen in zwei Beispielen verwenden. Bevor wir das tun, sehen wir uns an, wie die Parameterübergabe durch Abgleich erklärt werden kann.
Bei Funktionsaufrufen werden formale Parameter (innerhalb von Funktionsdefinitionen) gegen aktuelle Parameter (innerhalb von Funktionsaufrufen) abgeglichen. Als Beispiel nehmen wir die folgende Funktionsdefinition und den folgenden Funktionsaufruf.
Die Parameter a und b sind ähnlich wie die folgende Destrukturierung eingerichtet.
move2()Betrachten wir, wie die Destrukturierung für move2() funktioniert.
Beispiel 1. Der Funktionsaufruf move2() führt zu dieser Destrukturierung:
Das einzelne Array-Element auf der linken Seite hat keine Übereinstimmung auf der rechten Seite, weshalb {x,y} gegen den Standardwert und nicht gegen Daten von der rechten Seite abgeglichen wird (Regeln 3b, 3d).
Die linke Seite enthält Kurzformen für Eigenschaftswerte. Sie ist eine Abkürzung für
Diese Destrukturierung führt zu den folgenden beiden Zuweisungen (Regeln 2c, 1):
Das ist, was wir wollten. Im nächsten Beispiel sind wir jedoch nicht so glücklich.
Beispiel 2. Untersuchen wir den Funktionsaufruf move2({z: 3}), der zu folgender Destrukturierung führt:
Es gibt ein Array-Element an Index 0 auf der rechten Seite. Daher wird der Standardwert ignoriert und der nächste Schritt ist (Regel 3d):
Das führt dazu, dass sowohl x als auch y auf undefined gesetzt werden, was nicht das ist, was wir wollen. Das Problem ist, dass {x,y} nicht mehr gegen den Standardwert, sondern gegen {z:3} abgeglichen wird.
move1()Versuchen wir move1().
Beispiel 1: move1()
Wir haben kein Array-Element an Index 0 auf der rechten Seite und verwenden den Standardwert (Regel 3d).
Die linke Seite enthält Kurzformen für Eigenschaftswerte, was bedeutet, dass diese Destrukturierung äquivalent ist zu:
Weder die Eigenschaft x noch die Eigenschaft y haben eine Übereinstimmung auf der rechten Seite. Daher werden die Standardwerte verwendet und die folgenden Destrukturierungen werden als nächstes durchgeführt (Regel 2d):
Das führt zu den folgenden Zuweisungen (Regel 1):
Hier erhalten wir, was wir wollen. Sehen wir, ob unser Glück beim nächsten Beispiel anhält.
Beispiel 2: move1({z: 3})
Das erste Element des Array-Musters hat eine Übereinstimmung auf der rechten Seite, und diese Übereinstimmung wird verwendet, um die Destrukturierung fortzusetzen (Regel 3d).
Wie in Beispiel 1 gibt es keine Eigenschaften x und y auf der rechten Seite, und die Standardwerte werden verwendet.
Es funktioniert wie gewünscht! Diesmal ist das Muster, bei dem x und y gegen {z:3} abgeglichen werden, kein Problem, da sie eigene lokale Standardwerte haben.
Die Beispiele zeigen, dass Standardwerte ein Merkmal von Musterteilen (Objekteigenschaften oder Array-Elemente) sind. Wenn ein Teil keine Übereinstimmung hat oder gegen undefined abgeglichen wird, wird der Standardwert verwendet. Das heißt, das Muster wird stattdessen gegen den Standardwert abgeglichen.