Iterationsprotokolle
Iterationsprotokolle sind keine neuen integrierten Funktionen oder Syntaxelemente, sondern Protokolle. Jedes Objekt kann diese Protokolle implementieren, indem es bestimmte Konventionen einhält.
Es gibt zwei Protokolle: das Iterable-Protokoll und das Iterator-Protokoll.
Das Iterable-Protokoll
Das Iterable-Protokoll ermöglicht es JavaScript-Objekten, ihr Iterationsverhalten festzulegen oder anzupassen, etwa welche Werte in einer for...of-Schleife durchlaufen werden. Einige integrierte Typen sind integrierte Iterables mit einem standardmäßigen Iterationsverhalten, beispielsweise Array oder Map. Andere Typen, etwa Object, sind nicht iterierbar.
Damit ein Objekt iterierbar ist, muss es die Methode [Symbol.iterator]() implementieren. Das bedeutet, dass das Objekt selbst oder eines der Objekte in seiner Prototypenkette eine Eigenschaft mit dem Schlüssel [Symbol.iterator] besitzen muss, der über die Konstante Symbol.iterator verfügbar ist:
[Symbol.iterator]()-
Eine Funktion ohne Argumente, die ein Objekt zurückgibt, das dem Iterator-Protokoll entspricht.
Immer wenn über ein Objekt iteriert werden soll, beispielsweise zu Beginn einer for...of-Schleife, wird seine Methode [Symbol.iterator]() ohne Argumente aufgerufen. Der zurückgegebene Iterator wird verwendet, um die zu durchlaufenden Werte abzurufen.
Beachten Sie, dass diese Funktion ohne Argumente als Methode des iterierbaren Objekts aufgerufen wird. Innerhalb der Funktion kann daher über das Schlüsselwort this auf die Eigenschaften des iterierbaren Objekts zugegriffen werden, um zu entscheiden, welche Werte bei der Iteration bereitgestellt werden.
Diese Funktion kann eine gewöhnliche Funktion oder eine Generatorfunktion sein. In beiden Fällen gibt sie beim Aufruf ein Iteratorobjekt zurück. Innerhalb der Generatorfunktion können die einzelnen Einträge mit yield bereitgestellt werden.
Das Iterator-Protokoll
Das Iterator-Protokoll definiert eine einheitliche Methode, um eine endliche oder unendliche Folge von Werten zu erzeugen und gegebenenfalls einen Rückgabewert bereitzustellen, nachdem alle Werte erzeugt wurden.
Ein Objekt ist ein Iterator, wenn es eine Methode next() mit der folgenden Semantik implementiert:
next()-
Eine Funktion, die null oder ein Argument entgegennimmt und ein Objekt zurückgibt, das der Schnittstelle
IteratorResultentspricht (siehe unten). Wird ein Wert zurückgegeben, der kein Objekt ist, etwafalseoderundefined, während eine integrierte Sprachfunktion wiefor...ofden Iterator verwendet, wird einTypeError("iterator.next() returned a non-object value") ausgelöst.
Von allen Methoden des Iterator-Protokolls (next(), return() und throw()) wird erwartet, dass sie ein Objekt zurückgeben, das die Schnittstelle IteratorResult implementiert. Es hat die folgenden Eigenschaften:
doneOptional-
Ein boolescher Wert, der
falseist, wenn der Iterator den nächsten Wert der Folge erzeugen konnte. (Dies entspricht dem vollständigen Weglassen der Eigenschaftdone.)Der Wert ist
true, wenn der Iterator seine Folge abgeschlossen hat. In diesem Fall gibtvalueoptional den Rückgabewert des Iterators an. valueOptional-
Ein beliebiger JavaScript-Wert, den der Iterator zurückgibt. Kann weggelassen werden, wenn
doneden Werttruehat.
In der Praxis ist keine der beiden Eigenschaften zwingend erforderlich. Wird ein Objekt ohne diese Eigenschaften zurückgegeben, entspricht dies im Ergebnis { done: false, value: undefined }.
Wenn ein Iterator ein Ergebnis mit done: true zurückgibt, sollten auch alle späteren Aufrufe von next() done: true zurückgeben. Dies wird allerdings nicht auf Sprachebene erzwungen.
Die Methode next kann einen Wert entgegennehmen, der ihrem Funktionskörper zur Verfügung steht. Die einzige Sprachfunktion, die einen Wert übergibt, ist yield*. Der an die Methode next eines Generators übergebene Wert wird zum Wert des entsprechenden yield-Ausdrucks.
Optional kann der Iterator auch die Methoden return(value) und throw(exception) implementieren. Wenn sie aufgerufen werden, teilen sie dem Iterator mit, dass der Aufrufer die Iteration beendet, sodass der Iterator notwendige Bereinigungsarbeiten ausführen kann, beispielsweise eine Datenbankverbindung schließen.
return(value)Optional-
Eine Funktion, die null oder ein Argument entgegennimmt und ein Objekt zurückgibt, das der Schnittstelle
IteratorResultentspricht. Üblicherweise entspricht dessenvaluedem übergebenenvalue, unddoneisttrue. Der Aufruf dieser Methode teilt dem Iterator mit, dass der Aufrufer keine weiteren Aufrufe vonnext()beabsichtigt und Bereinigungsarbeiten durchgeführt werden können. Wenn integrierte Sprachfunktionenreturn()zur Bereinigung aufrufen, istvalueimmerundefined. throw(exception)Optional-
Eine Funktion, die null oder ein Argument entgegennimmt und ein Objekt zurückgibt, das der Schnittstelle
IteratorResultentspricht. Üblicherweise istdonedabeitrue. Der Aufruf dieser Methode teilt dem Iterator mit, dass der Aufrufer einen Fehlerzustand festgestellt hat.exceptionist üblicherweise eineError-Instanz. Keine integrierte Sprachfunktion ruftthrow()zur Bereinigung auf – es ist eine besondere Eigenschaft von Generatoren, die der Symmetrie vonreturnundthrowdient.
Hinweis:
Es lässt sich nicht durch Introspektion feststellen, ob ein bestimmtes Objekt das Iterator-Protokoll implementiert. Dazu müssen Sie next() tatsächlich aufrufen und das zurückgegebene Ergebnis überprüfen.
Es ist sehr einfach, einen Iterator zugleich iterierbar zu machen: Implementieren Sie einfach eine Methode [Symbol.iterator](), die this zurückgibt.
// Satisfies both the Iterator Protocol and Iterable
const myIterator = {
next() {
// …
},
[Symbol.iterator]() {
return this;
},
};
Ein solches Objekt wird als iterierbarer Iterator bezeichnet. Dadurch kann der Iterator mit den verschiedenen Syntaxformen verwendet werden, die iterierbare Objekte erwarten. Deshalb ist es selten sinnvoll, das Iterator-Protokoll zu implementieren, ohne das Objekt zugleich iterierbar zu machen. (Tatsächlich erwarten fast alle Syntaxformen und APIs iterierbare Objekte und keine Iteratoren.) Das Generatorobjekt ist ein Beispiel:
const generatorObject = (function* () {
yield 1;
yield 2;
yield 3;
})();
console.log(typeof generatorObject.next);
// "function" — it has a next method (which returns the right result), so it's an iterator
console.log(typeof generatorObject[Symbol.iterator]);
// "function" — it has a [Symbol.iterator] method (which returns the right iterator), so it's an iterable
console.log(generatorObject[Symbol.iterator]() === generatorObject);
// true — its [Symbol.iterator] method returns itself (an iterator), so it's an iterable iterator
Alle integrierten Iteratoren erben von Iterator.prototype. Dort ist die Methode [Symbol.iterator]() so implementiert, dass sie this zurückgibt. Somit sind integrierte Iteratoren auch iterierbar.
Wenn möglich, ist es jedoch besser, wenn iterable[Symbol.iterator]() jeweils unterschiedliche Iteratoren zurückgibt, die immer am Anfang beginnen, wie es beispielsweise Set.prototype[Symbol.iterator]() tut.
Die Protokolle für asynchrone Iteratoren und asynchrone Iterables
Für die asynchrone Iteration gibt es ein weiteres Protokollpaar: das Protokoll für asynchrone Iteratoren und das für asynchrone Iterables. Ihre Schnittstellen ähneln denen des Iterable- und des Iterator-Protokolls stark. Der Unterschied besteht darin, dass jeder Rückgabewert eines Aufrufs einer Iteratormethode in ein Promise eingebettet ist.
Ein Objekt implementiert das Protokoll für asynchrone Iterables, wenn es die folgende Methode implementiert:
[Symbol.asyncIterator]()-
Eine Funktion ohne Argumente, die ein Objekt zurückgibt, das dem Protokoll für asynchrone Iteratoren entspricht.
Ein Objekt implementiert das Protokoll für asynchrone Iteratoren, wenn es die folgenden Methoden implementiert:
next()-
Eine Funktion, die null oder ein Argument entgegennimmt und ein Promise zurückgibt. Das Promise wird mit einem Objekt erfüllt, das der Schnittstelle
IteratorResultentspricht. Dessen Eigenschaften haben dieselbe Semantik wie bei einem synchronen Iterator. return(value)Optional-
Eine Funktion, die null oder ein Argument entgegennimmt und ein Promise zurückgibt. Das Promise wird mit einem Objekt erfüllt, das der Schnittstelle
IteratorResultentspricht. Dessen Eigenschaften haben dieselbe Semantik wie bei einem synchronen Iterator. throw(exception)Optional-
Eine Funktion, die null oder ein Argument entgegennimmt und ein Promise zurückgibt. Das Promise wird mit einem Objekt erfüllt, das der Schnittstelle
IteratorResultentspricht. Dessen Eigenschaften haben dieselbe Semantik wie bei einem synchronen Iterator.
Zusammenspiel der Sprache mit den Iterationsprotokollen
Die Sprache definiert APIs, die iterierbare Objekte und Iteratoren erzeugen oder verwenden.
Integrierte Iterables
String, Array, TypedArray, Map, Set und Segments (zurückgegeben von Intl.Segmenter.prototype.segment()) sind integrierte Iterables, weil ihre jeweiligen prototype-Objekte eine Methode [Symbol.iterator]() implementieren. Auch das arguments-Objekt und einige DOM-Collection-Typen wie NodeList sind iterierbar.
Abgesehen von AsyncGenerator gibt es in der JavaScript-Kernsprache kein Objekt, das asynchron iterierbar ist. Bei manchen Web-APIs, beispielsweise ReadableStream, ist die Methode Symbol.asyncIterator standardmäßig vorhanden.
Generatorfunktionen geben Generatorobjekte zurück, die iterierbare Iteratoren sind. Asynchrone Generatorfunktionen geben asynchrone Generatorobjekte zurück, die asynchron iterierbare Iteratoren sind.
Die von integrierten Iterables zurückgegebenen Iteratoren erben tatsächlich alle von einer gemeinsamen Klasse, Iterator. Diese implementiert die zuvor erwähnte Methode [Symbol.iterator]() { return this; }, wodurch sie alle iterierbare Iteratoren sind. Die Klasse Iterator stellt neben der vom Iterator-Protokoll geforderten Methode next() zusätzliche Hilfsmethoden bereit. Sie können die Prototypenkette eines Iterators untersuchen, indem Sie ihn in einer grafischen Konsole ausgeben.
console.log([][Symbol.iterator]());
Array Iterator {}
[[Prototype]]: Array Iterator ==> This is the prototype shared by all array iterators
next: ƒ next()
Symbol(Symbol.toStringTag): "Array Iterator"
[[Prototype]]: Object ==> This is the prototype shared by all built-in iterators
Symbol(Symbol.iterator): ƒ [Symbol.iterator]()
[[Prototype]]: Object ==> This is Object.prototype
Integrierte APIs, die Iterables akzeptieren
Viele APIs akzeptieren iterierbare Objekte. Beispiele sind:
Map()WeakMap()Set()WeakSet()Promise.all()Promise.allSettled()Promise.race()Promise.any()Array.from()Object.groupBy()Map.groupBy()
const myObj = {};
new WeakSet(
(function* () {
yield {};
yield myObj;
yield {};
})(),
).has(myObj); // true
Syntaxformen, die Iterables erwarten
Einige Anweisungen und Ausdrücke erwarten iterierbare Objekte, beispielsweise for...of-Schleifen, die Spread-Syntax für Arrays und Parameter, yield* und die Array-Destrukturierung:
for (const value of ["a", "b", "c"]) {
console.log(value);
}
// "a"
// "b"
// "c"
console.log([..."abc"]); // ["a", "b", "c"]
function* gen() {
yield* ["a", "b", "c"];
}
console.log(gen().next()); // { value: "a", done: false }
[a, b, c] = new Set(["a", "b", "c"]);
console.log(a); // "a"
Wenn integrierte Syntaxformen einen Iterator durchlaufen und done im letzten Ergebnis false ist (der Iterator also weitere Werte erzeugen kann), aber keine weiteren Werte benötigt werden, wird die Methode return aufgerufen, sofern sie vorhanden ist. Das kann beispielsweise geschehen, wenn in einer for...of-Schleife ein break oder return erreicht wird oder bei einer Array-Destrukturierung bereits alle Bezeichner gebunden sind.
const obj = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
i++;
console.log("Returning", i);
if (i === 3) return { done: true, value: i };
return { done: false, value: i };
},
return() {
console.log("Closing");
return { done: true };
},
};
},
};
const [a] = obj;
// Returning 1
// Closing
const [b, c, d] = obj;
// Returning 1
// Returning 2
// Returning 3
// Already reached the end (the last call returned `done: true`),
// so `return` is not called
console.log([b, c, d]); // [1, 2, undefined]; the value associated with `done: true` is not reachable
for (const b of obj) {
break;
}
// Returning 1
// Closing
Die Verwendung von for...of, der Spread-Syntax für Arrays usw. mit einem asynchronen Iterable, das nicht zugleich synchron iterierbar ist (also [Symbol.asyncIterator](), aber kein [Symbol.iterator]() besitzt), löst einen TypeError aus: x is not iterable. Sie müssen Syntaxformen verwenden, die für asynchrone Iterables vorgesehen sind, darunter for await...of, yield* in asynchronen Generatorfunktionen (aber nicht in synchronen Generatorfunktionen) und Array.fromAsync(). Umgekehrt können diese Syntaxformen auch synchrone Iterables verarbeiten und deren Ergebnisse implizit in Promises umwandeln.
Fehlerbehandlung
Bei einer Iteration wechselt die Kontrolle zwischen dem Iterator und dem aufrufenden Code. Deshalb spielt die Fehlerbehandlung in beide Richtungen eine Rolle: wie der aufrufende Code mit Fehlern umgeht, die der Iterator auslöst, und wie der Iterator mit Fehlern umgeht, die der aufrufende Code auslöst. Wenn Sie eine der integrierten Iterationsmöglichkeiten verwenden, kann die Sprache auch Fehler auslösen, weil das Iterable bestimmte Invarianten verletzt. Im Folgenden wird beschrieben, wie integrierte Syntaxformen Fehler erzeugen und behandeln. Dies kann Ihnen als Orientierung dienen, wenn Sie einen Iterator manuell durchlaufen.
Nicht wohlgeformte Iterables
Beim Abrufen des Iterators aus einem Iterable können Fehler auftreten. Die Sprache erzwingt dabei die Invariante, dass das Iterable einen gültigen Iterator bereitstellen muss:
- Es besitzt eine aufrufbare Methode
[Symbol.iterator](). - Die Methode
[Symbol.iterator]()gibt ein Objekt zurück. - Das von
[Symbol.iterator]()zurückgegebene Objekt besitzt eine aufrufbare Methodenext().
Wenn Sie mit integrierter Syntax eine Iteration über ein nicht wohlgeformtes Iterable beginnen, wird ein TypeError ausgelöst.
const nonWellFormedIterable = { [Symbol.iterator]: 1 };
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable is not iterable
nonWellFormedIterable[Symbol.iterator] = () => 1;
[...nonWellFormedIterable]; // TypeError: [Symbol.iterator]() returned a non-object value
nonWellFormedIterable[Symbol.iterator] = () => ({});
[...nonWellFormedIterable]; // TypeError: nonWellFormedIterable[Symbol.iterator]().next is not a function
Hat bei einem asynchronen Iterable dessen Eigenschaft [Symbol.asyncIterator]() den Wert undefined oder null, verwendet JavaScript stattdessen die Eigenschaft [Symbol.iterator] und umhüllt den resultierenden Iterator mit einem asynchronen Iterator, indem es dessen Methoden weiterleitet. Andernfalls muss auch die Eigenschaft [Symbol.asyncIterator] die oben genannten Invarianten erfüllen.
Sie können diese Art von Fehler vermeiden, indem Sie das Iterable vor Beginn der Iteration prüfen. Solche Fehler sind allerdings recht selten, da Ihnen der Typ des Objekts, über das Sie iterieren, normalerweise bekannt ist. Wenn Sie das Iterable von anderem Code erhalten, sollten Sie den Fehler einfach an den Aufrufer weitergeben, damit dieser erkennt, dass eine ungültige Eingabe übergeben wurde.
Fehler während der Iteration
Die meisten Fehler treten beim Fortschreiten des Iterators auf, also beim Aufruf von next(). Die Sprache erzwingt dabei die Invariante, dass die Methode next() ein Objekt zurückgeben muss (bei asynchronen Iteratoren nach dem Abwarten). Andernfalls wird ein TypeError ausgelöst.
Wenn diese Invariante verletzt wird oder die Methode next() einen Fehler auslöst (bei asynchronen Iteratoren kann sie auch ein zurückgewiesenes Promise zurückgeben), wird der Fehler an den Aufrufer weitergegeben. Bei integrierten Syntaxformen wird die laufende Iteration ohne erneuten Versuch und ohne Bereinigung abgebrochen. Dabei wird angenommen, dass die Methode next() bereits selbst aufgeräumt hat, wenn sie den Fehler ausgelöst hat. Wenn Sie next() manuell aufrufen, können Sie den Fehler abfangen und next() erneut aufrufen. Im Allgemeinen sollten Sie jedoch davon ausgehen, dass der Iterator bereits geschlossen ist.
Wenn der aufrufende Code die Iteration aus einem anderen Grund als den im vorigen Absatz genannten Fehlern beendet, beispielsweise weil in seinem eigenen Code ein Fehlerzustand eintritt (etwa bei der Verarbeitung eines ungültigen Werts aus dem Iterator), sollte er die Methode return() des Iterators aufrufen, sofern sie vorhanden ist. Dadurch kann der Iterator notwendige Bereinigungsarbeiten ausführen. Die Methode return() wird nur bei einem vorzeitigen Abbruch aufgerufen. Gibt next() done: true zurück, wird return() nicht aufgerufen, da angenommen wird, dass der Iterator bereits aufgeräumt hat.
Auch die Methode return() kann ungültig sein! Die Sprache erzwingt ebenfalls, dass return() ein Objekt zurückgibt, und löst andernfalls einen TypeError aus. Wenn die Methode return() einen Fehler auslöst, wird dieser an den Aufrufer weitergegeben. Wird return() jedoch aufgerufen, weil im eigenen Code des Aufrufers ein Fehler aufgetreten ist, hat dieser ursprüngliche Fehler Vorrang vor dem Fehler aus return().
Üblicherweise implementiert der aufrufende Code die Fehlerbehandlung wie folgt:
try {
for (const value of iterable) {
// …
}
} catch (e) {
// Handle the error
}
Der catch-Block kann Fehler abfangen, die auftreten, wenn iterable kein gültiges Iterable ist, wenn next() einen Fehler auslöst, wenn return() einen Fehler auslöst (falls die for-Schleife vorzeitig endet) und wenn im Rumpf der for-Schleife ein Fehler ausgelöst wird.
Die meisten Iteratoren werden mit Generatorfunktionen implementiert. Daher zeigen wir, wie Generatorfunktionen typischerweise mit Fehlern umgehen:
function* gen() {
try {
yield doSomething();
yield doSomethingElse();
} finally {
cleanup();
}
}
Da hier kein catch-Block vorhanden ist, werden Fehler, die doSomething() oder doSomethingElse() auslösen, an den Aufrufer von gen weitergegeben. Werden diese Fehler innerhalb der Generatorfunktion abgefangen (was ebenfalls ratsam sein kann), kann die Generatorfunktion entscheiden, ob sie weitere Werte bereitstellt oder vorzeitig beendet wird. Für Generatoren, die Ressourcen offen halten, ist der finally-Block jedoch notwendig. Seine Ausführung ist gewährleistet: entweder beim letzten Aufruf von next() oder beim Aufruf von return().
Weiterleitung von Fehlern
Einige integrierte Syntaxformen umhüllen einen Iterator mit einem weiteren Iterator. Dazu gehören der von Iterator.from() erzeugte Iterator, Iterator-Hilfsmethoden (map(), filter(), take(), drop() und flatMap()), yield* sowie ein nicht sichtbarer Wrapper, der entsteht, wenn Sie asynchrone Iteration (for await...of, Array.fromAsync) mit synchronen Iteratoren verwenden. Der umhüllende Iterator ist dann dafür verantwortlich, Fehler zwischen dem inneren Iterator und dem Aufrufer weiterzuleiten.
- Umhüllende Iteratoren leiten die Methode
next()des inneren Iterators im Allgemeinen direkt weiter, einschließlich ihres Rückgabewerts und ausgelöster Fehler. Der asynchrone Wrapper eines synchronen Iterators wartet auf die Eigenschaftvaluedes zurückgegebenen Objekts und wandelt ausgelöste Fehler in Promise-Zurückweisungen um. Wird ein bereitgestelltes Promise zurückgewiesen, ruft der Wrapper die Methodereturn()des inneren Iterators auf, sofern sie vorhanden ist, bevor er sein eigenes Promise zurückweist. Löst dagegen die Methodenext()des inneren Iterators selbst einen Fehler aus, weist der Wrapper sein Promise zurück, ohnereturn()aufzurufen. - Umhüllende Iteratoren leiten die Methode
return()des inneren Iterators im Allgemeinen direkt weiter. Wenn der innere Iterator keine Methodereturn()besitzt, geben sie stattdessen{ done: true, value: undefined }zurück. Bei Iterator-Hilfsmethoden gilt: Wurde die Methodenext()der Hilfsmethode noch nicht aufgerufen, gibt der umhüllende Iterator nach einem gegebenenfalls versuchten Aufruf vonreturn()des inneren Iterators immer{ done: true, value: undefined }zurück. Dies entspricht dem Verhalten von Generatorfunktionen, bei denen die Ausführung den Ausdruckyield*noch nicht erreicht hat. yield*ist die einzige integrierte Syntaxform, die die Methodethrow()an den inneren Iterator weiterleitet. Wieyield*die Methodenreturn()undthrow()weiterleitet, erfahren Sie in der zugehörigen Referenz.
Beispiele
>Benutzerdefinierte Iterables
So können Sie eigene Iterables erstellen:
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
console.log([...myIterable]); // [1, 2, 3]
Einfacher Iterator
Iteratoren sind von Natur aus zustandsbehaftet. Wenn Sie den Iterator nicht als Generatorfunktion definieren (wie im obigen Beispiel), sollten Sie den Zustand wahrscheinlich in einer Closure kapseln.
function makeIterator(array) {
let nextIndex = 0;
return {
next() {
return nextIndex < array.length
? {
value: array[nextIndex++],
done: false,
}
: {
done: true,
};
},
};
}
const it = makeIterator(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true
Unendlicher Iterator
function idMaker() {
let index = 0;
return {
next() {
return {
value: index++,
done: false,
};
},
};
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// …
Ein Iterable mit einem Generator definieren
function* makeGenerator(array) {
let nextIndex = 0;
while (nextIndex < array.length) {
yield array[nextIndex++];
}
}
const gen = makeGenerator(["yo", "ya"]);
console.log(gen.next().value); // 'yo'
console.log(gen.next().value); // 'ya'
console.log(gen.next().done); // true
function* idMaker() {
let index = 0;
while (true) {
yield index++;
}
}
const it = idMaker();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 2
// …
Ein Iterable mit einer Klasse definieren
Der Zustand lässt sich auch mithilfe privater Felder kapseln.
class SimpleClass {
#data;
constructor(data) {
this.#data = data;
}
[Symbol.iterator]() {
// Use a new index for each iterator. This makes multiple
// iterations over the iterable safe for non-trivial cases,
// such as use of break or nested looping over the same iterable.
let index = 0;
return {
// Note: using an arrow function allows `this` to point to the
// one of `[Symbol.iterator]()` instead of `next()`
next: () => {
if (index >= this.#data.length) {
return { done: true };
}
return { value: this.#data[index++], done: false };
},
};
}
}
const simple = new SimpleClass([1, 2, 3, 4, 5]);
for (const val of simple) {
console.log(val); // 1 2 3 4 5
}
Integrierte Iterables überschreiben
Ein String ist beispielsweise ein integriertes iterierbares Objekt:
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"
Der Standarditerator von String gibt die Codepunkte des Strings einzeln zurück:
const iterator = someString[Symbol.iterator]();
console.log(`${iterator}`); // "[object String Iterator]"
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Sie können das Iterationsverhalten neu definieren, indem Sie eine eigene Methode [Symbol.iterator]() bereitstellen:
// need to construct a String object explicitly to avoid auto-boxing
const someString = new String("hi");
someString[Symbol.iterator] = function () {
return {
// this is the iterator object, returning a single element (the string "bye")
next() {
return this._first
? { value: "bye", done: (this._first = false) }
: { done: true };
},
_first: true,
};
};
Beachten Sie, wie sich die Neudefinition von [Symbol.iterator]() auf das Verhalten integrierter Konstrukte auswirkt, die das Iterationsprotokoll verwenden:
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"
Gleichzeitige Änderungen während der Iteration
Fast alle Iterables haben dieselbe grundlegende Semantik: Sie kopieren die Daten nicht zu Beginn der Iteration. Stattdessen halten sie einen Zeiger vor und verschieben ihn. Wenn Sie also während der Iteration Elemente einer Collection hinzufügen, löschen oder ändern, können Sie unbeabsichtigt beeinflussen, ob andere, unveränderte Elemente der Collection besucht werden. Das ähnelt stark der Funktionsweise iterativer Array-Methoden.
Betrachten Sie das folgende Beispiel mit URLSearchParams:
const searchParams = new URLSearchParams(
"deleteme1=value1&key2=value2&key3=value3",
);
// Delete unwanted keys
for (const [key, value] of searchParams) {
console.log(key);
if (key.startsWith("deleteme")) {
searchParams.delete(key);
}
}
// Output:
// deleteme1
// key3
Beachten Sie, dass key2 nie ausgegeben wird. Das liegt daran, dass URLSearchParams intern eine Liste von Schlüssel-Wert-Paaren verwendet. Wenn deleteme1 besucht und gelöscht wird, rücken alle anderen Einträge um eine Position nach links. Dadurch nimmt key2 die bisherige Position von deleteme1 ein. Wenn der Zeiger zum nächsten Schlüssel weiterwandert, landet er bei key3.
Einige Implementierungen von Iterables vermeiden dieses Problem, indem sie gelöschte Einträge mit einem Platzhalterwert markieren, statt die übrigen Werte zu verschieben. Betrachten Sie dazu ähnlichen Code mit einer Map:
const myMap = new Map([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myMap) {
console.log(key);
if (key.startsWith("deleteme")) {
myMap.delete(key);
}
}
// Output:
// deleteme1
// key2
// key3
Beachten Sie, dass alle Schlüssel ausgegeben werden. Das liegt daran, dass Map die übrigen Schlüssel nicht verschiebt, wenn einer gelöscht wird. Wenn Sie etwas Ähnliches implementieren möchten, könnte das so aussehen:
const tombstone = Symbol("tombstone");
class MyIterable {
#data;
constructor(data) {
this.#data = data;
}
delete(deletedKey) {
for (let i = 0; i < this.#data.length; i++) {
if (this.#data[i][0] === deletedKey) {
this.#data[i] = tombstone;
return true;
}
}
return false;
}
*[Symbol.iterator]() {
for (const data of this.#data) {
if (data !== tombstone) {
yield data;
}
}
}
}
const myIterable = new MyIterable([
["deleteme1", "value1"],
["key2", "value2"],
["key3", "value3"],
]);
for (const [key, value] of myIterable) {
console.log(key);
if (key.startsWith("deleteme")) {
myIterable.delete(key);
}
}
Warnung: Gleichzeitige Änderungen sind im Allgemeinen fehleranfällig und verwirrend. Wenn Sie nicht genau wissen, wie das Iterable implementiert ist, sollten Sie die Collection während der Iteration möglichst nicht verändern.
Spezifikationen
| Spezifikation |
|---|
| ECMAScript® 2027 Language Specification> # sec-iteration> |
Siehe auch
- Leitfaden zu Iteratoren und Generatoren
function*Symbol.iteratorIterator