Dieser Artikel enthält eine ausführliche Einführung in Iterables und Iteratoren in JavaScript. Meine Hauptmotivation für das Schreiben war die Vorbereitung auf das Erlernen von Generatoren. Tatsächlich wollte ich später mit der Kombination von Generatoren und React-Hooks experimentieren. Wenn Sie interessiert sind, folgen Sie meinem Twitter oder YouTube !
Eigentlich wollte ich mit einem Artikel über Generatoren beginnen, aber es wurde schnell klar, dass es schwierig ist, über sie zu sprechen, ohne ein gutes Verständnis für Iterables und Iteratoren zu haben. Wir werden uns jetzt auf sie konzentrieren. Ich gehe davon aus, dass Sie zu diesem Thema nichts wissen, aber wir werden uns gleichzeitig eingehend damit befassen. Also wenn du etwas bist Sie kennen sich mit Iterables und Iteratoren aus, fühlen sich aber nicht wohl, wenn Sie sie verwenden. Dieser Artikel hilft Ihnen dabei.
Einführung
Wie Sie bemerkt haben, diskutieren wir Iterables und Iteratoren. Diese sind miteinander verbunden, aber unterschiedliche Konzepte. Achten Sie daher beim Lesen des Artikels darauf, welches in einem bestimmten Fall diskutiert wird.
Beginnen wir mit iterierbaren Objekten. Was ist das? Dies kann beispielsweise wiederholt werden:
for (let element of iterable) {
// do something with an element
}
Bitte beachten Sie, dass wir hier nur Schleifen betrachten
for ... of
, die in ES6 eingeführt wurden. Und Schleifen
for ... in
sind ein älteres Konstrukt, auf das wir in diesem Artikel überhaupt nicht verweisen werden.
Jetzt denken Sie vielleicht: "Okay, diese iterierbare Variable ist nur ein Array!" Das ist richtig, Arrays sind iterierbar. Aber jetzt gibt es andere Strukturen in nativem JavaScript, die in einer Schleife verwendet werden können
for ... of
. Das heißt, neben Arrays gibt es noch andere iterierbare Objekte.
Zum Beispiel können wir iterieren
Map
, eingeführt in ES6:
const ourMap = new Map();
ourMap.set(1, 'a');
ourMap.set(2, 'b');
ourMap.set(3, 'c');
for (let element of ourMap) {
console.log(element);
}
Dieser Code zeigt Folgendes an:
[1, 'a']
[2, 'b']
[3, 'c']
Das heißt, die Variable
element
in jeder Iterationsstufe speichert ein Array von zwei Elementen. Der erste ist der Schlüssel, der zweite ist der Wert.
Dass wir eine Schleife
for ... of
zum Iterieren verwenden konnten,
Map
beweist, dass
Map
es iterierbar ist. Auch hier
for ... of
können nur iterierbare Objekte in Schleifen verwendet werden . Das heißt, wenn etwas mit dieser Schleife funktioniert, ist es ein iterierbares Objekt.
Es ist lustig, dass der Konstruktor
Map
optional Iterables von Schlüssel-Wert-Paaren akzeptiert. Das heißt, dies ist eine alternative Art, dasselbe zu konstruieren
Map
:
const ourMap = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c'],
]);
Und da es
Map
iterierbar ist, können wir sehr einfach Kopien davon erstellen:
const copyOfOurMap = new Map(ourMap);
Wir haben jetzt zwei verschiedene
Map
, obwohl sie dieselben Schlüssel mit denselben Werten speichern.
Wir haben also zwei Beispiele für iterierbare Objekte gesehen - Array und ES6
Map
. Aber wir wissen noch nicht, wie sie die Fähigkeit erhalten haben, iterierbar zu sein. Die Antwort ist einfach: Ihnen sind Iteratoren zugeordnet . Seien Sie vorsichtig: Iteratoren sind nicht iterierbar .
Wie ist ein Iterator einem iterierbaren Objekt zugeordnet? Eine einfache Iterable muss eine Funktion in ihrer Eigenschaft enthalten
Symbol.iterator
. Beim Aufruf muss die Funktion einen Iterator für dieses Objekt zurückgeben.
Sie können beispielsweise einen Array-Iterator abrufen:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
console.log(iterator);
Dieser Code wird an die Konsole ausgegeben
Object [Array Iterator] {}
. Jetzt wissen wir, dass dem Array ein Iterator zugeordnet ist, der eine Art Objekt ist.
Was ist ein Iterator?
Es ist einfach. Ein Iterator ist ein Objekt, das eine Methode enthält
next
. Wenn diese Methode aufgerufen wird, sollte sie Folgendes zurückgeben:
- der nächste Wert in einer Folge von Werten;
- Informationen darüber, ob der Iterator die Generierung von Werten abgeschlossen hat.
Testen wir dies, indem wir eine Methode
next
in unserem Array-Iterator aufrufen :
const result = iterator.next();
console.log(result);
Wir werden das Objekt in der Konsole sehen
{ value: 1, done: false }
. Das erste Element des Arrays, das wir erstellt haben, ist 1, und hier wurde es als Wert angezeigt. Wir haben auch Informationen erhalten, dass der Iterator noch nicht fertig ist, dh wir können die Funktion weiterhin aufrufen
next
und einige Werte abrufen . Lass es uns versuchen! Nennen wir
next
es noch zweimal:
console.log(iterator.next());
console.log(iterator.next());
Eins nach dem anderen erhalten
{ value: 2, done: false }
und
{ value: 3, done: false }
.
Es gibt nur drei Elemente in unserem Array. Was passiert, wenn Sie es erneut anrufen
next
?
console.log(iterator.next());
Diesmal werden wir sehen
{ value: undefined, done: true }
. Dies zeigt an, dass der Iterator vollständig ist. Es macht keinen Sinn, erneut anzurufen
next
. Wenn wir dies tun, erhalten wir immer wieder ein Objekt
{ value: undefined, done: true }
.
done: true
bedeutet aufhören zu iterieren.
Jetzt können Sie verstehen, was es
for ... of
unter der Haube tut :
- Die erste Methode
[Symbol.iterator]()
wird aufgerufen, um den Iterator abzurufen. - Die Methode
next
wird auf dem Iterator zyklisch aufgerufen, bis wir sie erhaltendone: true
. - Nach jedem Aufruf
next
wird die Eigenschaft im Hauptteil der Schleife verwendetvalue
.
Schreiben wir das alles in Code:
const iterator = ourArray[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const element = result.value;
// do some something with element
result = iterator.next();
}
Dieser Code entspricht dem:
for (let element of ourArray) {
// do something with element
}
Sie können dies beispielsweise überprüfen, indem Sie
console.log(element)
anstelle eines Kommentars einfügen
// do something with element
.
Erstellen Sie Ihren eigenen Iterator
Wir wissen jetzt, was Iterables und Iteratoren sind. Es stellt sich die Frage: "Kann ich meine eigenen Instanzen schreiben?"
Bestimmt!
Iteratoren sind nichts Geheimnisvolles. Dies sind nur Objekte mit einer Methode
next
, die sich auf besondere Weise verhalten. Wir haben bereits herausgefunden, welche nativen Werte in JS iterierbar sind. Unter ihnen wurden keine Gegenstände erwähnt. In der Tat werden sie nicht nativ wiederholt. Betrachten Sie ein Objekt wie dieses:
const ourObject = {
1: 'a',
2: 'b',
3: 'c'
};
Wenn wir damit iterieren
for (let element of ourObject)
, erhalten wir einen Fehler
object is not iterable
.
Schreiben wir unsere eigenen Iteratoren, indem wir ein solches Objekt iterierbar machen!
Dazu müssen Sie den Prototyp
Object
mit Ihrer eigenen Methode patchen
[Symbol.iterator]()
. Da das Patchen des Prototyps eine schlechte Praxis ist, erstellen wir unsere eigene Klasse, indem wir Folgendes erweitern
Object
:
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
}
Der Konstruktor unserer Klasse nimmt ein gewöhnliches Objekt und kopiert seine Eigenschaften in ein iterierbares Objekt (obwohl es noch nicht iterierbar ist!).
Erstellen wir ein iterierbares Objekt:
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
})
Um eine Klasse
IterableObject
wirklich iterierbar zu machen, benötigen wir eine Methode
[Symbol.iterator]()
. Fügen wir es hinzu.
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
[Symbol.iterator]() {
}
}
Jetzt können Sie einen echten Iterator schreiben!
Wir wissen bereits, dass es sich um ein Objekt mit einer Methode handeln muss
next
. Beginnen wir damit.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
return {
next() {}
}
}
}
Nach jedem Aufruf müssen
next
Sie ein Ansichtsobjekt zurückgeben
{ value, done }
. Machen wir es mit fiktiven Werten.
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
Bei einem iterierbaren Objekt wie diesem:
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
})
Wir werden Schlüssel-Wert-Paare ausgeben, ähnlich wie bei der ES6-Iteration
Map
:
['1', 'a']
['2', 'b']
['3', 'c']
In unserem Iterator
property
speichern wir ein Array im Wert
[key, valueForThatKey]
. Bitte beachten Sie, dass dies im Vergleich zu den vorherigen Schritten unsere eigene Lösung ist. Wenn wir einen Iterator schreiben wollten, der nur Schlüssel oder nur Werte von Eigenschaften zurückgibt, könnten wir dies ohne Probleme tun. Wir haben gerade beschlossen, Schlüssel-Wert-Paare zurückzugeben.
Wir brauchen ein Array vom Typ
[key, valueForThatKey]
. Der einfachste Weg, es zu bekommen, ist mit der Methode
Object.entries
. Wir können es direkt vor dem Erstellen des Iteratorobjekts in der Methode verwenden
[Symbol.iterator]()
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// we made an addition here
const entries = Object.entries(this);
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
Der in der Methode zurückgegebene Iterator greift dank des JavaScript-Abschlusses auf die Variable zu
entries
.
Wir brauchen auch eine Zustandsvariable. Hier erfahren wir, welches Schlüssel-Wert-Paar beim nächsten Aufruf zurückgegeben werden soll
next
. Fügen wir es hinzu:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
// we made an addition here
let index = 0;
return {
next() {
return {
value: undefined,
done: false
}
}
}
}
}
Beachten Sie, dass wir die Variable
index
c deklariert haben,
let
da wir wissen, dass wir ihren Wert nach jedem Aufruf aktualisieren möchten
next
.
Wir sind jetzt bereit, den tatsächlichen Wert in der Methode zurückzugeben
next
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
return {
// we made a change here
value: entries[index],
done: false
}
}
}
}
}
Es war einfach. Wir verwenden nur Variablen
entries
und
index
greifen vom Array aus auf das richtige Schlüssel-Wert-Paar zu
entries
.
Jetzt müssen wir uns mit der Immobilie befassen
done
, denn jetzt wird es immer so sein
false
. Sie können neben
entries
und eine weitere Variable erstellen und
index
diese nach jedem Aufruf aktualisieren
next
. Aber es gibt noch einen einfacheren Weg. Lassen Sie uns überprüfen, ob
index
das Array außerhalb der Grenzen liegt
entries
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
return {
value: entries[index],
// we made a change here
done: index >= entries.length
}
}
}
}
}
Unser Iterator endet, wenn die Variable
index
gleich oder größer als ihre Länge ist
entries
. Wenn y beispielsweise die
entries
Länge 3 hat, enthält es Werte an den Indizes 0, 1 und 2. Wenn die Variable
index
gleich oder größer als 3 ist, bedeutet dies, dass keine Werte mehr vorhanden sind. Wir sind fertig.
Dieser Code funktioniert fast . Es gibt nur noch eins hinzuzufügen.
Die Variable
index
beginnt bei 0, aber ... wir aktualisieren sie nicht! Es ist nicht so einfach. Wir müssen die Variable aktualisieren, nachdem wir zurückgekehrt sind
{ value, done }
. Aber als wir es zurückgaben, die Methode
next
stoppt sofort, auch wenn nach dem Ausdruck Code steht
return
. Wir können jedoch ein Objekt erstellen
{ value, done }
, es in einer Variablen speichern, aktualisieren
index
und erst dann das Objekt zurückgeben:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
const result = {
value: entries[index],
done: index >= entries.length
};
index++;
return result;
}
}
}
}
Nach unseren Änderungen
IterableObject
sieht die Klasse folgendermaßen aus:
class IterableObject extends Object {
constructor(object) {
super();
Object.assign(this, object);
}
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = 0;
return {
next() {
const result = {
value: entries[index],
done: index >= entries.length
};
index++;
return result;
}
}
}
}
Der Code funktioniert gut, wurde aber ziemlich verwirrend. Dies liegt daran, dass die Aktualisierung
index
nach der Objekterstellung intelligenter, aber weniger offensichtlich ist
result
. Wir können einfach
index
auf -1 initialisieren ! Und obwohl es aktualisiert wird, bevor das Objekt zurückkehrt
next
, funktioniert alles einwandfrei, da das erste Update -1 durch 0 ersetzt.
Also machen wir es:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
const entries = Object.entries(this);
let index = -1;
return {
next() {
index++;
return {
value: entries[index],
done: index >= entries.length
}
}
}
}
}
Wie Sie sehen, müssen wir jetzt die Reihenfolge der Objekterstellung
result
und -aktualisierung nicht mehr ändern
index
. Während des zweiten Aufrufs wird es
index
auf 1 aktualisiert und wir geben ein anderes Ergebnis zurück und so weiter. Alles funktioniert wie gewünscht und der Code sieht viel einfacher aus.
Aber wie überprüfen wir die Richtigkeit der Arbeit? Sie können manuell eine Methode ausführen,
[Symbol.iterator]()
um einen Iterator zu instanziieren und dann die Ergebnisse der Aufrufe direkt zu überprüfen
next
. Aber Sie können viel einfacher machen! Es wurde oben gesagt, dass jedes iterierbare Objekt in eine Schleife eingefügt werden kann
for ... of
. Lassen Sie uns genau das tun und dabei die von unserem iterierbaren Objekt zurückgegebenen Werte protokollieren:
const iterableObject = new IterableObject({
1: 'a',
2: 'b',
3: 'c'
});
for (let element of iterableObject) {
console.log(element);
}
Funktioniert! Folgendes wird in der Konsole angezeigt:
[ '1', 'a' ]
[ '2', 'b' ]
[ '3', 'c' ]
Cool! Wir haben mit einem Objekt begonnen, das nicht in Schleifen verwendet werden konnte
for ... of
, da diese keine integrierten Iteratoren enthalten. Aber wir haben unseren eigenen erstellt
IterableObject
, dem ein selbstgeschriebener Iterator zugeordnet ist.
Ich hoffe, Sie können jetzt das Potenzial von Iterables und Iteratoren erkennen. Es ist ein Mechanismus, mit dem Sie Ihre eigenen Datenstrukturen erstellen können, um mit JS-Funktionen wie Schleifen zu arbeiten
for ... of
, und sie funktionieren genau wie native Strukturen! Dies ist eine sehr nützliche Funktion, die Ihren Code in bestimmten Situationen erheblich vereinfachen kann, insbesondere wenn Sie vorhaben, Ihre Datenstrukturen häufig zu iterieren.
Darüber hinaus können wir anpassen, was genau diese Iterationen zurückgeben sollen. Unser Iterator gibt jetzt Schlüssel-Wert-Paare zurück. Was ist, wenn wir nur Werte wollen? Einfach, schreiben Sie einfach den Iterator neu:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// changed `entries` to `values`
const values = Object.values(this);
let index = -1;
return {
next() {
index++;
return {
// changed `entries` to `values`
value: values[index],
// changed `entries` to `values`
done: index >= values.length
}
}
}
}
}
Und alle! Wenn wir jetzt die Schleife starten
for ... of
, sehen wir in der Konsole:
a b c
Wir haben nur die Werte der Objekte zurückgegeben. All dies beweist die Flexibilität selbstgeschriebener Iteratoren. Sie können sie zurückgeben lassen, was Sie wollen.
Iteratoren als ... iterierbare Objekte
Es ist sehr häufig, dass Menschen Iteratoren und Iterables verwechseln. Dies ist ein Fehler und ich habe versucht, die beiden sauber zu trennen. Ich vermute, ich kenne den Grund, warum die Leute sie so oft verwirren.
Es stellt sich heraus, dass Iteratoren ... manchmal iterabel sind!
Was bedeutet das? Denken Sie daran, ein Iterable ist das Objekt, dem ein Iterator zugeordnet ist. Jeder native JavaScript-Iterator verfügt über eine Methode
[Symbol.iterator]()
, die einen anderen Iterator zurückgibt! Dies macht den ersten Iterator zu einem iterierbaren Objekt.
Sie können dies überprüfen, indem Sie einen von einem Array zurückgegebenen Iterator verwenden und ihn aufrufen
[Symbol.iterator]()
:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
const secondIterator = iterator[Symbol.iterator]();
console.log(secondIterator);
Nachdem Sie diesen Code ausgeführt haben, werden Sie sehen
Object [Array Iterator] {}
. Das heißt, ein Iterator enthält nicht nur einen anderen Iterator, sondern auch ein Array.
Wenn Sie beide Iteratoren mit using vergleichen,
===,
stellt sich heraus, dass sie genau gleich sind:
const iterator = ourArray[Symbol.iterator]();
const secondIterator = iterator[Symbol.iterator]();
// logs `true`
console.log(iterator === secondIterator);
Am Anfang mag es seltsam sein, wie sich ein Iterator verhält, der sein eigener Iterator ist. Dies ist jedoch eine sehr nützliche Funktion. Sie können einen nackten Iterator nicht in eine Schleife stecken
for ... of
, er akzeptiert nur ein iterierbares Objekt - ein Objekt mit einer Methode
[Symbol.iterator]()
.
Die Situation, in der ein Iterator sein eigener Iterator (und daher ein iterierbares Objekt) ist, verbirgt jedoch das Problem. Da native JS-Iteratoren Methoden enthalten
[Symbol.iterator]()
, können Sie diese ohne zu zögern direkt an Schleifen übergeben
for ... of
.
Als Ergebnis dieses Snippets:
const ourArray = [1, 2, 3];
for (let element of ourArray) {
console.log(element);
}
und das hier:
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
nahtlos arbeiten und das Gleiche tun. Aber warum sollte jemand solche Iteratoren direkt in Schleifen verwenden
for ... of
? Manchmal ist es einfach unvermeidlich.
Zunächst müssen Sie möglicherweise einen Iterator erstellen, ohne zu einem iterierbaren Element zu gehören. Wir werden uns dieses Beispiel unten ansehen, und es ist nicht ungewöhnlich. Manchmal brauchen wir das Iterable selbst einfach nicht.
Und es wäre sehr umständlich, wenn ein nackter Iterator bedeuten würde, dass Sie ihn nicht verwenden können
for ... of
. Natürlich können Sie dies manuell mit einer Methode
next
und beispielsweise einer Schleife tun
while
, aber wir haben gesehen, dass Sie dafür viel Code schreiben müssen, außerdem repetitiv.
Die Lösung ist einfach: Wenn Sie Boilerplate-Code vermeiden und einen Iterator in einer Schleife verwenden möchten
for ... of
, müssen Sie den Iterator zu einem iterierbaren Objekt machen.
Andererseits erhalten wir Iteratoren auch ziemlich oft von anderen Methoden als
[Symbol.iterator]()
. Zum Beispiel ES6
Map
enthält Methoden
entries
,
values
und
keys
. Sie alle geben Iteratoren zurück.
Wenn native JS-Iteratoren nicht auch iterierbare Objekte wären, könnten Sie diese Methoden nicht direkt in Schleifen verwenden
for ... of
, wie folgt:
for (let element of map.entries()) {
console.log(element);
}
for (let element of map.values()) {
console.log(element);
}
for (let element of map.keys()) {
console.log(element);
}
Dieser Code funktioniert, da die von den Methoden zurückgegebenen Iteratoren auch iterierbare Objekte sind. Andernfalls müssten Sie beispielsweise das
map.entries()
Aufrufergebnis in ein dummes iterierbares Objekt einschließen. Zum Glück müssen wir das nicht tun.
Es wird als bewährte Methode angesehen, eigene iterierbare Objekte zu erstellen. Vor allem, wenn sie von anderen Methoden als zurückgegeben werden
[Symbol.iterator]()
. Es ist sehr einfach, einen Iterator zu einem iterierbaren Objekt zu machen. Lassen Sie mich anhand eines Beispiels eines Iterators zeigen
IterableObject
:
class IterableObject extends Object {
// same as before
[Symbol.iterator]() {
// same as before
return {
next() {
// same as before
},
[Symbol.iterator]() {
return this;
}
}
}
}
Wir haben eine Methode
[Symbol.iterator]()
unterhalb der Methode erstellt
next
. Machte diesen Iterator zu seinem eigenen Iterator, indem er einfach zurückkehrte
this
, was bedeutet, dass er sich selbst zurückgibt. Oben haben wir bereits gesehen, wie sich ein Array-Iterator verhält. Dies reicht aus, damit unser Iterator
for ... of
auch direkt in Schleifen arbeitet .
Iteratorzustand
Es sollte nun offensichtlich sein, dass jedem Iterator ein Status zugeordnet ist. In einem Iterator haben
IterableObject
wir beispielsweise einen Status - eine Variable
index
- als Abschluss gespeichert . Und wir haben es nach jedem Iterationsschritt aktualisiert.
Was passiert nach Abschluss des Iterationsprozesses? Der Iterator wird unbrauchbar und Sie können (sollten!) Ihn löschen. Sie können sehen, dass dies auch am Beispiel nativer JS-Objekte geschieht. Nehmen wir einen Array-Iterator und versuchen Sie, ihn zweimal in einer Schleife auszuführen
for ... of
.
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
for (let element of iterator) {
console.log(element);
}
Sie können erwarten, dass die Konsole die Zahlen zweimal anzeigt
1
,
2
und
3
. Aber das Ergebnis wird so aussehen:
1
2
3
Warum?
Rufen wir
next
nach dem Ende der Schleife manuell auf :
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
console.log(iterator.next());
Das letzte Protokoll wird an die Konsole ausgegeben
{ value: undefined, done: true }
.
Das ist es. Nach dem Ende der Schleife geht der Iterator in den Zustand "Fertig". Jetzt wird immer ein Objekt zurückgegeben
{ value: undefined, done: true }
.
Gibt es eine Möglichkeit, den Status des Iterators zurückzusetzen, damit er ein zweites Mal verwendet werden kann
for ... of
? In einigen Fällen ist es möglich, aber es macht keinen Sinn. Daher ist es
[Symbol.iterator]
eine Methode, nicht nur eine Eigenschaft. Sie können die Methode erneut aufrufen und einen weiteren Iterator abrufen :
const ourArray = [1, 2, 3];
const iterator = ourArray[Symbol.iterator]();
for (let element of iterator) {
console.log(element);
}
const secondIterator = ourArray[Symbol.iterator]();
for (let element of secondIterator) {
console.log(element);
}
Alles funktioniert jetzt wie erwartet. Mal sehen, warum mehrere Vorwärtsschleifen durch das Array funktionieren:
const ourArray = [1, 2, 3];
for (let element of ourArray) {
console.log(element);
}
for (let element of ourArray) {
console.log(element);
}
Alle Schleifen
for ... of
verwenden unterschiedliche Iteratoren! Sobald der Iterator und die Schleife beendet sind, wird dieser Iterator nicht mehr verwendet.
Iteratoren und Arrays
Da wir Iteratoren (wenn auch indirekt) in Schleifen verwenden
for ... of
, können sie täuschend wie Arrays aussehen. Es gibt jedoch zwei wichtige Unterschiede. Iterator und Array verwenden die Konzepte gieriger und fauler Werte. Wenn Sie ein Array erstellen, hat es zu einem bestimmten Zeitpunkt eine bestimmte Länge und seine Werte sind bereits initialisiert. Natürlich können Sie ein Array ohne Werte erstellen, aber das ist nicht der Fall. Mein Punkt ist, dass es unmöglich ist, ein Array zu erstellen, das seine Werte erst initialisiert, nachdem Sie durch Schreiben darauf zugegriffen haben
array[someIndex]
. Es ist möglicherweise möglich, dies mit einem Proxy oder einem anderen Trick zu umgehen, aber standardmäßig verhalten sich JavaScript-Arrays nicht so.
Und wenn sie sagen, dass ein Array eine Länge hat, meinen sie, dass diese Länge endlich ist. In JavaScript gibt es keine unendlichen Arrays.
Diese beiden Eigenschaften zeigen an, dass Arrays gierig sind .
Und Iteratoren sind faul .
Um dies zu zeigen, erstellen wir zwei unserer Iteratoren: Der erste ist im Gegensatz zu endlichen Arrays unendlich, und der zweite initialisiert seine Werte nur, wenn sie vom Iteratorbenutzer angefordert werden.
Beginnen wir mit einem unendlichen Iterator. Klingt einschüchternd, ist aber sehr einfach zu erstellen: Der Iterator beginnt bei 0 und gibt bei jedem Schritt die nächste Zahl in der Sequenz zurück. Für immer.
const counterIterator = {
integer: -1,
next() {
this.integer++;
return { value: this.integer, done: false };
},
[Symbol.iterator]() {
return this;
}
}
Und alle! Wir haben mit einer Eigenschaft von
integer
-1 begonnen. Bei jedem Aufruf
next
erhöhen wir ihn um 1 und geben ihn im Objekt als zurück
value
. Beachten Sie, dass wir den oben genannten Trick erneut angewendet haben: Wir haben bei -1 begonnen, um beim ersten Mal 0 zurückzugeben.
Schauen Sie sich auch die Eigenschaft an
done
. Es wird immer falsch sein. Dieser Iterator endet nicht!
Darüber hinaus haben wir den Iterator durch eine einfache Implementierung iterierbar gemacht
[Symbol.iterator]()
.
Eine letzte Sache: Dies ist der Fall, den ich oben erwähnt habe - wir haben einen Iterator erstellt, aber es wird kein iterierbares übergeordnetes Element benötigt, um zu funktionieren.
Versuchen wir nun diesen Iterator in einer Schleife
for ... of
. Sie müssen nur daran denken, die Schleife irgendwann zu stoppen, sonst wird der Code für immer ausgeführt.
for (let element of counterIterator) {
if (element > 5) {
break;
}
console.log(element);
}
Nach dem Start sehen wir in der Konsole:
0
1
2
3
4
5
Wir haben tatsächlich einen unendlichen Iterator erstellt, der so viele Zahlen zurückgibt, wie Sie möchten. Und es war sehr einfach, es zu schaffen!
Schreiben wir nun einen Iterator, der erst dann Werte erstellt, wenn diese angefordert werden.
Nun ... wir haben es schon geschafft!
Haben Sie bemerkt, dass jeweils
counterIterator
nur eine Immobiliennummer gespeichert ist
integer
? Dies ist die letzte Nummer, die beim Anruf zurückgegeben wird
next
. Und das ist die gleiche Faulheit. Ein Iterator kann möglicherweise eine beliebige Zahl zurückgeben (genauer gesagt eine positive Ganzzahl). Sie werden jedoch nur dann erstellt, wenn sie benötigt werden: wenn die Methode aufgerufen wird
next
.
Das kann ziemlich knifflig aussehen. Schließlich werden Zahlen schnell erstellt und beanspruchen nicht viel Speicherplatz. Wenn Sie jedoch mit sehr großen Objekten arbeiten, die viel Speicher beanspruchen, kann das Ersetzen von Arrays durch Iteratoren manchmal sehr nützlich sein, um das Programm zu beschleunigen und Speicherplatz zu sparen.
Je größer das Objekt ist (oder je länger die Erstellung dauert), desto größer ist der Nutzen.
Andere Möglichkeiten zur Verwendung von Iteratoren
Bisher haben wir nur Iteratoren in einer Schleife
for ... of
oder manuell mit dem verwendet
next
. Dies sind jedoch nicht die einzigen Möglichkeiten.
Wir haben bereits gesehen, dass der Konstruktor
Map
iterables als Argument verwendet. Mit der Methode können Sie auch
Array.from
einfach eine iterable Datei in ein Array konvertieren. Aber sei vorsichtig! Wie gesagt, die Faulheit des Iterators kann manchmal ein großer Vorteil sein. Das Konvertieren in ein Array nimmt Faulheit. Alle vom Iterator zurückgegebenen Werte werden sofort initialisiert und dann in ein Array eingefügt. Dies bedeutet, dass wenn wir versuchen, unendlich
counterIterator
in ein Array umzuwandeln , dies zu einer Katastrophe führt.
Array.from
wird für immer ausgeführt, ohne ein Ergebnis zurückzugeben. Bevor Sie einen Iterable / Iterator in ein Array konvertieren, müssen Sie sicherstellen, dass der Vorgang sicher ist.
Interessanterweise funktionieren iterables auch gut mit dem Spread-Operator
(...
.) Denken Sie daran, dass dies genauso funktioniert
Array.from
, wenn alle Iteratorwerte gleichzeitig generiert werden. Beispielsweise können Sie mit dem Spread-Operator eine eigene Version erstellen
Array.from
. Wenden Sie einfach den Operator auf die Iterable an und fügen Sie die Werte in ein Array ein:
const arrayFromIterator = [...iterable];
Sie können auch alle Werte aus dem iterierbaren Objekt abrufen und auf die Funktion anwenden:
someFunction(...iterable);
Fazit
Ich hoffe, Sie verstehen jetzt den Titel des Artikels Iterable Objects and Iterators. Wir haben gelernt, was sie sind, wie sie sich unterscheiden, wie man sie verwendet und wie man eigene Instanzen erstellt. Wir sind jetzt völlig bereit, mit Generatoren zu arbeiten. Wenn Sie mit Iteratoren vertraut sind, sollte es nicht allzu schwierig sein, mit dem nächsten Thema fortzufahren.