Iterationsprotokolle
Iterationsprotokolle sind keine neuen eingebauten Funktionen oder Syntaxen, sondern Protokolle. Diese Protokolle können von jedem Objekt durch Befolgung bestimmter Konventionen implementiert werden.
Es gibt zwei Protokolle: Das iterable Protokoll und das Iterator-Protokoll.
Das iterable Protokoll
Das iterable Protokoll ermöglicht es JavaScript-Objekten, ihr Iterationsverhalten zu definieren oder anzupassen, wie zum Beispiel welche Werte in einer for...of
-Konstruktion durchlaufen werden. Einige eingebaute Typen sind eingebaute Iterables mit einem Standard-Iterationsverhalten, wie Array
oder Map
, während andere Typen (wie Object
) dies nicht sind.
Um iterierbar zu sein, muss ein Objekt die Methode [Symbol.iterator]()
implementieren. Das bedeutet, dass das Objekt (oder eines der Objekte in seiner Prototypkette) eine Eigenschaft mit einem [Symbol.iterator]
-Schlüssel haben muss, welcher über die Konstante Symbol.iterator
verfügbar ist:
[Symbol.iterator]()
-
Eine Funktion ohne Argumente, die ein Objekt zurückgibt, das dem Iterator-Protokoll entspricht.
Wann immer ein Objekt iteriert werden muss (wie z.B. am Anfang einer for...of
-Schleife), wird seine Methode [Symbol.iterator]()
ohne Argumente aufgerufen, und der zurückgegebene Iterator wird verwendet, um die zu iterierenden Werte zu erhalten.
Beachten Sie, dass, wenn diese Funktion ohne Argumente aufgerufen wird, sie als Methode des iterierbaren Objekts aufgerufen wird. Daher kann innerhalb der Funktion das this
-Schlüsselwort verwendet werden, um auf die Eigenschaften des iterierbaren Objekts zuzugreifen, um zu entscheiden, was während der Iteration bereitgestellt werden soll.
Diese Funktion kann eine gewöhnliche Funktion oder eine Generatorfunktion sein, so dass beim Aufruf ein Iterator-Objekt zurückgegeben wird. Innerhalb dieser Generatorfunktion kann jeder Eintrag mit yield
bereitgestellt werden.
Das Iterator-Protokoll
Das Iterator-Protokoll definiert eine standardisierte Methode, um eine Sequenz von Werten (entweder endlich oder unendlich) zu erzeugen und möglicherweise einen Rückgabewert anzugeben, wenn alle Werte erzeugt wurden.
Ein Objekt ist ein Iterator, wenn es eine next()
-Methode mit den folgenden Semantiken implementiert:
next()
-
Eine Funktion, die null oder ein Argument akzeptiert und ein Objekt zurückgibt, das der
IteratorResult
-Schnittstelle entspricht (siehe unten). Wenn ein Nicht-Objekt-Wert zurückgegeben wird (wiefalse
oderundefined
), während ein eingebautes Sprachfeature (wiefor...of
) den Iterator verwendet, wird einTypeError
("iterator.next() returned a non-object value"
) ausgelöst.
Alle Methoden des Iterator-Protokolls (next()
, return()
, und throw()
) werden erwartet, dass sie ein Objekt zurückgeben, das die IteratorResult
-Schnittstelle implementiert. Es muss die folgenden Eigenschaften haben:
done
Optional-
Ein boolescher Wert, der
false
ist, wenn der Iterator in der Lage war, den nächsten Wert in der Sequenz zu erzeugen. (Dies entspricht dem Nicht-Spezifizieren derdone
-Eigenschaft überhaupt.)Hat den Wert
true
, wenn der Iterator seine Sequenz abgeschlossen hat. In diesem Fall gibtvalue
optional den Rückgabewert des Iterators an. value
Optional-
Ein beliebiger JavaScript-Wert, der vom Iterator zurückgegeben wird. Kann weggelassen werden, wenn
done
true
ist.
In der Praxis ist keine der Eigenschaften strikt erforderlich; wenn ein Objekt ohne eine der Eigenschaften zurückgegeben wird, ist es effektiv gleichbedeutend mit { done: false, value: undefined }
.
Wenn ein Iterator ein Ergebnis mit done: true
zurückgibt, wird erwartet, dass alle nachfolgenden Aufrufe von next()
ebenfalls done: true
zurückgeben, obwohl dies auf der Sprachebene nicht erzwungen wird.
Die next
-Methode kann einen Wert empfangen, der dem Methodenkörper zur Verfügung gestellt wird. Kein eingebautes Sprachfeature wird irgendeinen Wert übergeben. Der an die next
-Methode von Generatoren übergebene Wert wird zum Wert des korrespondierenden yield
-Ausdrucks.
Optional kann der Iterator auch die Methoden return(value)
und throw(exception)
implementieren, die, wenn sie aufgerufen werden, dem Iterator mitteilen, dass der Aufrufer mit der Iteration fertig ist und etwaige notwendige Aufräumarbeiten (wie das Schließen der Datenbankverbindung) ausgeführt werden können.
return(value)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Objekt zurückgibt, das der
IteratorResult
-Schnittstelle entspricht, typischerweise mitvalue
gleich dem übergebenenvalue
-Wert unddone
gleichtrue
. Der Aufruf dieser Methode teilt dem Iterator mit, dass der Aufrufer nicht mehr die Absicht hat,next()
-Aufrufe durchzuführen, und Aufräumaktionen durchgeführt werden können. Wenn eingebaute Sprachfeaturesreturn()
für Aufräumarbeiten aufrufen, istvalue
immerundefined
. throw(exception)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Objekt zurückgibt, das der
IteratorResult
-Schnittstelle entspricht, typischerweise mitdone
gleichtrue
. Der Aufruf dieser Methode teilt dem Iterator mit, dass der Aufrufer eine Fehlersituation erkennt, undexception
ist typischerweise eineError
-Instanz. Kein eingebautes Sprachfeature ruftthrow()
für Aufräumzwecke auf – es ist eine Spezialität von Generatoren zur Symmetrie vonreturn
/throw
.
Hinweis:
Es ist nicht möglich, reflexiv (d.h. ohne tatsächlich next()
aufzurufen und das zurückgegebene Ergebnis zu validieren) festzustellen, ob ein bestimmtes Objekt das Iterator-Protokoll implementiert.
Es ist sehr einfach, einen Iterator auch iterierbar zu machen: Implementieren Sie einfach eine [Symbol.iterator]()
-Methode, 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 ein Iterator von den verschiedenen Syntaxen, die Iterables erwarten, konsumiert werden – daher ist es selten nützlich, das Iterator-Protokoll zu implementieren, ohne auch Iterable zu implementieren. (Tatsächlich erwarten fast alle Syntaxen und APIs Iterables, keine Iterators.) Das Generatorobjekt ist ein Beispiel:
const aGeneratorObject = (function* () {
yield 1;
yield 2;
yield 3;
})();
console.log(typeof aGeneratorObject.next);
// "function" — it has a next method (which returns the right result), so it's an iterator
console.log(typeof aGeneratorObject[Symbol.iterator]);
// "function" — it has an [Symbol.iterator] method (which returns the right iterator), so it's an iterable
console.log(aGeneratorObject[Symbol.iterator]() === aGeneratorObject);
// true — its [Symbol.iterator] method returns itself (an iterator), so it's an iterable iterator
Alle eingebauten Iterators erben von Iterator.prototype
, das die [Symbol.iterator]()
-Methode so implementiert, dass this
zurückgegeben wird, so dass eingebauten Iterators auch iterierbar sind.
Es ist jedoch besser, wenn möglich, dass iterable[Symbol.iterator]()
verschiedene Iterators zurückgibt, die immer vom Anfang starten, wie Set.prototype[Symbol.iterator]()
es tut.
Das asynchrone Iterator- und asynchrone Iterable-Protokoll
Es gibt ein weiteres Paar von Protokollen, die für asynchrone Iteration verwendet werden, die asynchrones Iterator- und asynchrones Iterable-Protokoll genannt werden. Sie haben sehr ähnliche Schnittstellen im Vergleich zu den Iterable- und Iterator-Protokollen, außer dass jeder Rückgabewert aus den Aufrufen der Iteratormethoden in einem Promise verpackt ist.
Ein Objekt implementiert das asynchrone Iterable-Protokoll, wenn es die folgenden Methoden implementiert:
[Symbol.asyncIterator]()
-
Eine Funktion ohne Argumente, die ein Objekt zurückgibt, das dem asynchronen Iterator-Protokoll entspricht.
Ein Objekt implementiert das asynchrone Iterator-Protokoll, wenn es die folgenden Methoden implementiert:
next()
-
Eine Funktion, die null oder ein Argument akzeptiert und ein Promise zurückgibt. Das Promise erfüllt ein Objekt, das der
IteratorResult
-Schnittstelle entspricht, und die Eigenschaften haben die gleichen Semantiken wie die des synchronen Iterators. return(value)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Promise zurückgibt. Das Promise erfüllt ein Objekt, das der
IteratorResult
-Schnittstelle entspricht, und die Eigenschaften haben die gleichen Semantiken wie die des synchronen Iterators. throw(exception)
Optional-
Eine Funktion, die null oder ein Argument akzeptiert und ein Promise zurückgibt. Das Promise erfüllt ein Objekt, das der
IteratorResult
-Schnittstelle entspricht, und die Eigenschaften haben die gleichen Semantiken wie die des synchronen Iterators.
Interaktionen zwischen der Sprache und den Iterationsprotokollen
Die Sprache spezifiziert APIs, die entweder Iterables oder Iterators erzeugen oder konsumieren.
Eingebaute Iterables
String
, Array
, TypedArray
, Map
, Set
, und Segments
(zurückgegeben von Intl.Segmenter.prototype.segment()
) sind alle eingebaute Iterables, weil jedes ihrer prototype
-Objekte eine [Symbol.iterator]()
-Methode implementiert. Zusätzlich sind das arguments
-Objekt und einige DOM-Sammlungstypen wie NodeList
ebenfalls Iterables. Im Kern der JavaScript-Sprache gibt es kein Objekt, das asynchron iterierbar ist. Einige Web-APIs, wie ReadableStream
, haben die Symbol.asyncIterator
-Methode standardmäßig gesetzt.
Generatorfunktionen geben Generatorobjekte zurück, die iterierbare Iteratoren sind. Asynchrone Generatorfunktionen geben asynchrone Generatorobjekte zurück, die asynchrone iterierbare Iteratoren sind.
Die von eingebauten Iterables zurückgegebenen Iterators erben tatsächlich alle von einer gemeinsamen Klasse Iterator
, die die zuvor erwähnte [Symbol.iterator]() { return this; }
-Methode implementiert, wodurch sie alle iterierbare Iteratoren sind. Die Iterator
-Klasse bietet auch zusätzliche Hilfsmethoden zusätzlich zur next()
-Methode, die vom Iterator-Protokoll gefordert wird. Sie können die Prototypkette eines Iterators inspizieren, indem Sie ihn in einer grafischen Konsole protokollieren.
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
Eingebaute APIs, die Iterables akzeptieren
Es gibt viele APIs, die Iterables akzeptieren. Einige 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
Syntaxen, die Iterables erwarten
Einige Anweisungen und Ausdrücke erwarten Iterables, z.B. die for...of
-Schleifen, Array- und Parameter-Spreading, yield*
, und 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 eingebaute Syntaxen einen Iterator durchlaufen und das letzte Ergebnis done
als false
ist (d.h. der Iterator kann mehr Werte erzeugen), aber keine weiteren Werte benötigt werden, wird die return
-Methode, falls vorhanden, aufgerufen. Dies kann passieren, zum Beispiel wenn ein break
oder return
in einer for...of
-Schleife auftritt oder wenn alle Bezeichner bereits in einer Array-Destrukturierung 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 for await...of
-Schleife und yield*
in asynchronen Generatorfunktionen (aber nicht in synchronen Generatorfunktionen) sind die einzigen Möglichkeiten, mit asynchronen Iterables zu interagieren. Die Verwendung von for...of
, Array-Spreading, usw. auf einem asynchronen Iterable, das nicht auch synchron iterierbar ist (d.h. es hat [Symbol.asyncIterator]()
aber kein [Symbol.iterator]()
), wird einen TypeError: x is not iterable werfen.
Fehlerbehandlung
Da beim Iterieren die Kontrolle hin- und her zwischen dem Iterator und dem Konsumenten übertragen wird, erfolgt die Fehlerbehandlung auf beiderlei Weise: wie der Konsument Fehler behandelt, die vom Iterator geworfen werden, und wie der Iterator Fehler behandelt, die vom Konsumenten geworfen werden. Wenn Sie eine der eingebauten Methoden zur Iteration verwenden, kann die Sprache auch Fehler werfen, weil das Iterable bestimmte Invarianten verletzt. Wir werden beschreiben, wie eingebaute Syntaxen Fehler erzeugen und behandeln, die als Leitfaden für Ihren eigenen Code verwendet werden können, wenn Sie den Iterator manuell steuern.
Nicht wohlgeformte Iterables
Fehler können auftreten, wenn der Iterator aus dem Iterable erworben wird. Die hier durchgesetzte Sprachinvariante ist, dass das Iterable einen gültigen Iterator erzeugen muss:
- Es hat eine aufrufbare
[Symbol.iterator]()
-Methode. - Die
[Symbol.iterator]()
-Methode gibt ein Objekt zurück. - Das von
[Symbol.iterator]()
zurückgegebene Objekt hat eine aufrufbarenext()
-Methode.
Wenn Sie eine eingebaute Syntax verwenden, um mit der Iteration eines nicht wohlgeformten Iterables zu beginnen, wird ein TypeError geworfen.
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
Für asynchrone Iterables, wenn seine [Symbol.asyncIterator]()
-Eigenschaft den Wert undefined
oder null
hat, greift JavaScript auf die Verwendung der [Symbol.iterator]
-Eigenschaft zurück (und umhüllt den resultierenden Iterator in einen asynchronen Iterator, indem es die Methoden weiterleitet). Andernfalls muss die [Symbol.asyncIterator]
-Eigenschaft ebenfalls den oben genannten Invarianten entsprechen.
Diese Art von Fehlern kann verhindert werden, indem das Iterable zunächst validiert wird, bevor versucht wird, es zu iterieren. Es ist jedoch ziemlich selten, da Sie normalerweise den Typ des Objekts kennen, über das Sie iterieren. Wenn Sie dieses Iterable von einem anderen Code erhalten, sollten Sie den Fehler einfach an den Aufrufer propagieren lassen, so dass er weiß, dass eine ungültige Eingabe bereitgestellt wurde.
Fehler während der Iteration
Die meisten Fehler treten auf, wenn der Iterator fortschreitet (Aufruf von next()
). Die hier durchgesetzte Sprachinvariante ist, dass die next()
-Methode ein Objekt zurückgeben muss (für asynchrone Iteratoren ein Objekt nach dem Warten). Andernfalls wird ein TypeError geworfen.
Wenn die Invariante verletzt wird oder die next()
-Methode einen Fehler wirft (für asynchrone Iteratoren kann sie auch ein abgelehntes Promise zurückgeben), wird der Fehler an den Aufrufer weitergegeben. Bei eingebauten Syntaxen wird die in Bearbeitung befindliche Iteration abgebrochen, ohne einen Wiederholungsversuch oder eine Aufräumbemühung (angenommen, dass die next()
-Methode den Fehler wirft, dann hat sie bereits aufgeräumt). Wenn Sie next()
manuell aufrufen, können Sie den Fehler abfangen und next()
erneut versuchen, aber allgemein sollten Sie davon ausgehen, dass der Iterator bereits geschlossen ist.
Wenn der Aufrufer beschließt, die Iteration aus einem anderen Grund als den Fehlern im vorherigen Absatz zu beenden, wie wenn er einen Fehlerzustand in seinem eigenen Code eingeht (zum Beispiel, während er einen ungültigen Wert bearbeitet, den der Iterator erzeugt hat), sollte er die return()
-Methode am Iterator aufrufen, falls eine existiert. Dies ermöglicht dem Iterator, etwaige Aufräumarbeiten durchzuführen. Die return()
-Methode wird nur für vorzeitige Beendigungen aufgerufen – wenn next()
done: true
zurückgibt, wird die return()
-Methode nicht aufgerufen, unter der Annahme, dass der Iterator bereits aufgeräumt hat.
Die return()
-Methode könnte ebenfalls ungültig sein! Die Sprache erzwingt auch, dass die return()
-Methode ein Objekt zurückgeben muss und wirft einen TypeError sonst. Wenn die return()
-Methode einen Fehler wirft, wird der Fehler an den Aufrufer weitergegeben. Wenn die return()
-Methode jedoch aufgerufen wird, weil der Aufrufer einen Fehler in seinem eigenen Code festgestellt hat, dann überschreibt dieser Fehler den von der return()
-Methode geworfenen Fehler.
Normalerweise implementiert der Aufrufer die Fehlerbehandlung wie folgt:
try {
for (const value of iterable) {
// …
}
} catch (e) {
// Handle the error
}
Der catch
kann Fehler abfangen, die geworfen werden, wenn iterable
kein gültiges Iterable ist, wenn next()
einen Fehler wirft, wenn return()
einen Fehler wirft (wenn die for
-Schleife frühzeitig beendet wird), und wenn der Körper der for
-Schleife einen Fehler wirft.
Die meisten Iteratoren werden mit Generatorfunktionen implementiert, daher demonstrieren wir, wie Generatorfunktionen typischerweise Fehler behandeln:
function* gen() {
try {
yield doSomething();
yield doSomethingElse();
} finally {
cleanup();
}
}
Das Fehlen von catch
hier führt dazu, dass Fehler, die von doSomething()
oder doSomethingElse()
geworfen werden, an den Aufrufer von gen
weitergegeben werden. Wenn diese Fehler innerhalb der Generatorfunktion abgefangen werden (was gleichermaßen ratsam ist), kann die Generatorfunktion entscheiden, weiterhin Werte zu produzieren oder frühzeitig zu beenden. Jedoch ist der finally
-Block notwendig für Generatoren, die Ressourcen offen halten. Der finally
-Block wird garantiert ausgeführt, wenn die letzte next()
aufgerufen wird oder wenn return()
aufgerufen wird.
Fehler weiterleiten
Einige eingebaute Syntaxen umhüllen einen Iterator in einen anderen Iterator. Dazu gehören der von Iterator.from()
erzeugte Iterator, Iterator-Hilfsmethoden (map()
, filter()
, take()
, drop()
, und flatMap()
), yield*
, und ein versteckter Wrapper, wenn Sie die asynchrone Iteration (for await...of
, Array.fromAsync
) auf synchronen Iteratoren verwenden. Der umhüllte Iterator ist dann dafür verantwortlich, Fehler zwischen dem inneren Iterator und dem Aufrufer weiterzuleiten.
- Alle Wrapper-Iterators leiten die
next()
-Methode des inneren Iterators direkt weiter, einschließlich ihres Rückgabe- und Fehlerwurfwerts. - Wrapper-Iterators leiten im Allgemeinen die
return()
-Methode des inneren Iterators direkt weiter. Wenn diereturn()
-Methode beim inneren Iterator nicht existiert, gibt sie stattdessen{ done: true, value: undefined }
zurück. Im Fall von Iterator-Hilfsmethoden: Wenn dienext()
-Methode des Iterator-Helfers noch nicht aufgerufen wurde, gibt der aktuelle Iterator nach dem Versuch,return()
am inneren Iterator zu rufen, immer{ done: true, value: undefined }
zurück. Dies ist konsistent mit Generatorfunktionen, bei denen die Ausführung noch nicht in denyield*
-Ausdruck eingetreten ist. yield*
ist die einzige eingebaute Syntax, die diethrow()
-Methode des inneren Iterators weiterleitet. Für Informationen darüber, wieyield*
diereturn()
- undthrow()
-Methoden weiterleitet, sehen Sie sich die eigene Referenz an.
Beispiele
Benutzerdefinierte Iterables
Sie können Ihre eigenen Iterables so 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 ihn nicht als Generatorfunktion definieren (wie das obige Beispiel zeigt), möchten 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
// …
Definieren eines Iterables mit einem Generator
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
// …
Definieren eines Iterables mit einer Klasse
Die Kapselung des Zustands kann mit privaten Feldern ebenfalls durchgeführt werden.
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
}
Überschreiben von eingebauten Iterables
Zum Beispiel ist ein String
ein eingebautes iterierbares Objekt:
const someString = "hi";
console.log(typeof someString[Symbol.iterator]); // "function"
String
's Standard-Iterator gibt die Codepunkte des Strings nacheinander 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 unser eigenes [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 das Neudefinieren von [Symbol.iterator]()
das Verhalten von eingebauten Konstrukten beeinflusst, die das Iterationsprotokoll verwenden:
console.log([...someString]); // ["bye"]
console.log(`${someString}`); // "hi"
Gleichzeitige Modifikationen beim Iterieren
Fast alle Iterables haben das gleiche zugrunde liegende Semantik: Sie kopieren die Daten nicht zu dem Zeitpunkt, an dem die Iteration beginnt. Vielmehr behalten sie einen Zeiger und bewegen ihn herum. Daher, wenn Sie Elemente in der Sammlung hinzufügen, löschen oder ändern, während Sie über die Sammlung iterieren, können Sie unbeabsichtigt ändern, ob andere unveränderte Elemente in der Sammlung besucht werden. Dies ist sehr ähnlich zu den iterativen Array-Methoden.
Betrachten Sie den folgenden Fall mit einem 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, wie key2
niemals protokolliert wird. Das liegt daran, dass URLSearchParams
grundlegend eine Liste von Schlüssel-Wert-Paaren ist. Wenn deleteme1
besucht und gelöscht wird, werden alle anderen Einträge um eins nach links verschoben, so dass key2
die Position einnimmt, die zuvor deleteme1
hatte, und wenn der Zeiger zum nächsten Schlüssel bewegt wird, landet er auf key3
.
Gewisse Iterables-Implementierungen vermeiden dieses Problem, indem sie "Grabstein"–Werte setzen, um das Verschieben der verbleibenden Werte zu vermeiden. Betrachten Sie den ä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, wie alle Schlüssel protokolliert werden. Das liegt daran, dass Map
die verbleibenden Schlüssel nicht verschiebt, wenn einer gelöscht wird. Wenn Sie etwas ähnliches implementieren möchten, kann es 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 Modifikationen sind im Allgemeinen sehr fehleranfällig und verwirrend. Es sei denn, Sie wissen genau, wie das Iterable implementiert ist, ist es am besten, die Sammlung während des Iterierens nicht zu modifizieren.
Spezifikationen
Specification |
---|
ECMAScript® 2026 Language Specification # sec-iteration |