Iterierbare Objekte sind eine Verallgemeinerung von Arrays. Das ist ein Konzept, das es uns ermöglicht, jedes Objekt in einer for..of
Schleife nutzbar zu machen.
Natürlich sind Arrays iterierbar. Es gibt aber auch viele andere integrierte Objekte, die ebenfalls iterierbar sind. Strings sind beispielsweise auch iterierbar.
Wenn ein Objekt technisch gesehen kein Array ist, sondern eine Sammlung (Liste, Menge) von etwas darstellt, dann ist for..of
eine großartige Syntax, um eine Schleife darüber durchzuführen. Sehen wir uns also an, wie es funktioniert.
Wir können das Konzept der Iterables leicht verstehen, indem wir eines unserer eigenen erstellen.
Zum Beispiel haben wir ein Objekt, das kein Array ist, aber für for..of
geeignet aussieht.
Wie ein range
, das ein Intervall von Zahlen darstellt:
let range = { ab: 1, bis: 5 }; // Wir wollen, dass for..of funktioniert: // for(let num of range) ... num=1,2,3,4,5
Um das range
iterierbar zu machen (und damit for..of
arbeiten zu lassen), müssen wir dem Objekt eine Methode namens Symbol.iterator
hinzufügen (ein spezielles integriertes Symbol nur für diesen Zweck).
Wenn for..of
startet, ruft es diese Methode einmal auf (oder es treten Fehler auf, wenn sie nicht gefunden wird). Die Methode muss einen Iterator zurückgeben – ein Objekt mit der Methode next
.
for..of
funktioniert nur mit diesem zurückgegebenen Objekt .
Wenn for..of
den nächsten Wert möchte, ruft es next()
für dieses Objekt auf.
Das Ergebnis von next()
muss die Form {done: Boolean, value: any}
haben, wobei done=true
bedeutet, dass die Schleife beendet ist, andernfalls ist value
der nächste Wert.
Hier ist die vollständige Implementierung für range
mit Anmerkungen:
let range = { ab: 1, bis: 5 }; // 1. Aufruf von for..of ruft dies zunächst auf range[Symbol.iterator] = function() { // ...es gibt das Iteratorobjekt zurück: // 2. Weiter funktioniert for..of nur mit dem Iteratorobjekt unten und fragt es nach den nächsten Werten zurückkehren { aktuell: this.from, zuletzt: this.to, // 3. next() wird bei jeder Iteration von der for..of-Schleife aufgerufen nächste() { // 4. Es sollte den Wert als Objekt zurückgeben {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } anders { return { done: true }; } } }; }; // jetzt funktioniert es! for (sei die Anzahl des Bereichs) { alarm(num); // 1, dann 2, 3, 4, 5 }
Bitte beachten Sie das Kernmerkmal von Iterables: Trennung von Anliegen.
Der range
selbst verfügt nicht über die Methode next()
.
Stattdessen wird durch den Aufruf von range[Symbol.iterator]()
ein weiteres Objekt, ein sogenannter „Iterator“, erstellt, dessen next()
Werte für die Iteration generiert.
Das Iteratorobjekt ist also von dem Objekt getrennt, über das es iteriert.
Technisch gesehen können wir sie zusammenführen und range
selbst als Iterator verwenden, um den Code einfacher zu machen.
So was:
let range = { ab: 1, bis: 5, [Symbol.iterator]() { this.current = this.from; gib dies zurück; }, nächste() { if (this.current <= this.to) { return { done: false, value: this.current++ }; } anders { return { done: true }; } } }; for (sei die Anzahl des Bereichs) { alarm(num); // 1, dann 2, 3, 4, 5 }
Nun gibt range[Symbol.iterator]()
das range
Objekt selbst zurück: Es verfügt über die notwendige next()
Methode und merkt sich den aktuellen Iterationsfortschritt in this.current
. Kürzer? Ja. Und manchmal ist das auch in Ordnung.
Der Nachteil ist, dass es jetzt unmöglich ist, zwei for..of
-Schleifen gleichzeitig über das Objekt laufen zu lassen: Sie teilen sich den Iterationsstatus, da es nur einen Iterator gibt – das Objekt selbst. Aber zwei parallele For-Ofs kommen selten vor, selbst in asynchronen Szenarios.
Unendliche Iteratoren
Auch unendliche Iteratoren sind möglich. Beispielsweise wird der range
unendlich für range.to = Infinity
. Oder wir können ein iterierbares Objekt erstellen, das eine unendliche Folge von Pseudozufallszahlen generiert. Kann auch nützlich sein.
Für next
gibt es keine Einschränkungen, es können immer mehr Werte zurückgegeben werden, das ist normal.
Natürlich wäre die for..of
Schleife über eine solche Iteration endlos. Aber wir können es jederzeit mit break
stoppen.
Arrays und Strings sind die am häufigsten verwendeten integrierten Iterables.
Für eine Zeichenfolge durchläuft for..of
die darin enthaltenen Zeichen:
for (let char of „test“) { // wird viermal ausgelöst: einmal für jedes Zeichen alarm( char ); // t, dann e, dann s, dann t }
Und es funktioniert korrekt mit Ersatzpaaren!
let str = '??'; for (let char of str) { alarm( char ); // ?, und dann ? }
Für ein tieferes Verständnis sehen wir uns an, wie man einen Iterator explizit verwendet.
Wir iterieren über einen String auf genau die gleiche Weise wie for..of
, jedoch mit direkten Aufrufen. Dieser Code erstellt einen String-Iterator und ruft daraus „manuell“ Werte ab:
let str = "Hallo"; // macht das Gleiche wie // for (let char of str) alarm(char); let iterator = str[Symbol.iterator](); while (wahr) { let result = iterator.next(); if (result.done) break; alarm(result.value); // gibt Zeichen einzeln aus }
Das ist selten nötig, gibt uns aber mehr Kontrolle über den Prozess als for..of
. Beispielsweise können wir den Iterationsprozess aufteilen: ein wenig iterieren, dann anhalten, etwas anderes tun und später fortfahren.
Zwei offizielle Begriffe sehen ähnlich aus, sind aber sehr unterschiedlich. Bitte stellen Sie sicher, dass Sie sie gut verstehen, um Verwirrung zu vermeiden.
Iterables sind Objekte, die die oben beschriebene Symbol.iterator
-Methode implementieren.
Array-likes sind Objekte mit Indizes und length
, sodass sie wie Arrays aussehen.
Wenn wir JavaScript für praktische Aufgaben in einem Browser oder einer anderen Umgebung verwenden, stoßen wir möglicherweise auf Objekte, die iterierbar oder arrayartig sind oder beides.
Beispielsweise sind Zeichenfolgen sowohl iterierbar ( for..of
funktioniert auf ihnen) als auch arrayartig (sie haben numerische Indizes und length
).
Ein iterierbares Element ist jedoch möglicherweise nicht arrayartig. Und umgekehrt ist ein Array-ähnliches Element möglicherweise nicht iterierbar.
Der range
im obigen Beispiel ist beispielsweise iterierbar, aber nicht arrayartig, da er keine indizierten Eigenschaften und length
hat.
Und hier ist das Objekt, das arrayartig, aber nicht iterierbar ist:
let arrayLike = { // hat Indizes und Länge => arrayartig 0: „Hallo“, 1: „Welt“, Länge: 2 }; // Fehler (kein Symbol.iterator) for (let item of arrayLike) {}
Sowohl Iterables als auch Array-Likes sind normalerweise keine Arrays , sie haben kein push
, pop
usw. Das ist ziemlich unpraktisch, wenn wir ein solches Objekt haben und damit wie mit einem Array arbeiten wollen. Beispielsweise möchten wir mit range
mithilfe von Array-Methoden arbeiten. Wie erreicht man das?
Es gibt eine universelle Methode Array.from, die einen iterierbaren oder Array-ähnlichen Wert annimmt und daraus ein „echtes“ Array
erstellt. Dann können wir darauf Array-Methoden aufrufen.
Zum Beispiel:
let arrayLike = { 0: „Hallo“, 1: „Welt“, Länge: 2 }; let arr = Array.from(arrayLike); // (*) alarm(arr.pop()); // Welt (Methode funktioniert)
Array.from
in der Zeile (*)
nimmt das Objekt, prüft es auf iterierbare oder arrayähnliche Eigenschaften, erstellt dann ein neues Array und kopiert alle Elemente dorthin.
Das Gleiche passiert für ein iterierbares:
// unter der Annahme, dass der Bereich dem obigen Beispiel entnommen ist let arr = Array.from(range); alarm(arr); // 1,2,3,4,5 (Array-zu-String-Konvertierung funktioniert)
Die vollständige Syntax für Array.from
ermöglicht es uns auch, eine optionale „Mapping“-Funktion bereitzustellen:
Array.from(obj[, mapFn, thisArg])
Das optionale zweite Argument mapFn
kann eine Funktion sein, die auf jedes Element angewendet wird, bevor es dem Array hinzugefügt wird. Mit thisArg
können wir this
dafür festlegen.
Zum Beispiel:
// unter der Annahme, dass der Bereich dem obigen Beispiel entnommen ist // quadriere jede Zahl let arr = Array.from(range, num => num * num); alarm(arr); // 1,4,9,16,25
Hier verwenden wir Array.from
um einen String in ein Array von Zeichen umzuwandeln:
let str = '??'; // teilt str in ein Array von Zeichen auf let chars = Array.from(str); alarm(chars[0]); // ? alarm(chars[1]); // ? alarm(chars.length); // 2
Im Gegensatz zu str.split
basiert es auf der iterierbaren Natur der Zeichenfolge und funktioniert daher genau wie for..of
korrekt mit Ersatzpaaren.
Technisch gesehen funktioniert es hier genauso wie:
let str = '??'; let chars = []; // Array.from führt intern die gleiche Schleife aus for (let char of str) { chars.push(char); } alarm(chars);
…Aber es ist kürzer.
Wir können sogar ein ersatzbewusstes slice
darauf aufbauen:
Funktion Slice(str, start, end) { return Array.from(str).slice(start, end).join(''); } let str = '???'; alarm( Slice(str, 1, 3) ); // ?? // Die native Methode unterstützt keine Ersatzpaare alarm( str.slice(1, 3) ); // Müll (zwei Teile aus verschiedenen Ersatzpaaren)
Objekte, die in for..of
verwendet werden können, werden als iterable bezeichnet.
Technisch gesehen müssen Iterables die Methode namens Symbol.iterator
implementieren.
Das Ergebnis von obj[Symbol.iterator]()
wird als Iterator bezeichnet. Es verwaltet den weiteren Iterationsprozess.
Ein Iterator muss über die Methode next()
verfügen, die ein Objekt zurückgibt {done: Boolean, value: any}
, hier bezeichnet done:true
das Ende des Iterationsprozesses, andernfalls ist der value
der nächste Wert.
Die Methode Symbol.iterator
wird automatisch von for..of
aufgerufen, wir können dies aber auch direkt tun.
Integrierte Iterables wie Strings oder Arrays implementieren auch Symbol.iterator
.
Der String-Iterator kennt Ersatzpaare.
Objekte mit indizierten Eigenschaften und length
werden als arrayartig bezeichnet. Solche Objekte können auch andere Eigenschaften und Methoden haben, ihnen fehlen jedoch die integrierten Methoden von Arrays.
Wenn wir einen Blick in die Spezifikation werfen, werden wir feststellen, dass die meisten integrierten Methoden davon ausgehen, dass sie mit iterierbaren oder Array-ähnlichen statt mit „echten“ Arrays arbeiten, weil das abstrakter ist.
Array.from(obj[, mapFn, thisArg])
erstellt ein echtes Array
aus einem iterierbaren oder Array-ähnlichen obj
, und wir können dann Array-Methoden darauf verwenden. Die optionalen Argumente mapFn
und thisArg
ermöglichen es uns, auf jedes Element eine Funktion anzuwenden.