argumentsfor-of-SchleifeArray.from()...)yield*return() und throw()ES6 führt einen neuen Mechanismus zur Durchquerung von Daten ein: Iteration. Zwei Konzepte sind zentral für die Iteration
Symbol.iterator. Diese Methode ist eine Fabrik für Iteratoren.Ausgedrückt als Schnittstellen in TypeScript-Notation, sehen diese Rollen so aus
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
}
interface IteratorResult {
value: any;
done: boolean;
}
Die folgenden Werte sind iterierbar
Einfache Objekte sind nicht iterierbar (warum das so ist, wird in einem eigenen Abschnitt erklärt).
Sprachkonstrukte, die über Iteration auf Daten zugreifen
const [a,b] = new Set(['a', 'b', 'c']);
for-of-Schleife for (const x of ['a', 'b', 'c']) {
console.log(x);
}
Array.from():
const arr = Array.from(new Set(['a', 'b', 'c']));
...) const arr = [...new Set(['a', 'b', 'c'])];
const map = new Map([[false, 'no'], [true, 'yes']]);
const set = new Set(['a', 'b', 'c']);
Promise.all(), Promise.race() Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);
yield*:
yield* anIterable;
Die Idee der Iterierbarkeit ist folgende.
for-of-Schleife über Werte und der Spread-Operator (...) fügt Werte in Arrays oder Funktionsaufrufe ein.Es ist nicht praktikabel, dass jeder Konsument alle Quellen unterstützt, zumal es möglich sein sollte, neue Quellen zu erstellen (z. B. über Bibliotheken). Daher führt ES6 die Schnittstelle Iterable ein. Datenkonsumenten verwenden sie, Datenquellen implementieren sie.
Da JavaScript keine Schnittstellen hat, ist Iterable eher eine Konvention.
Symbol.iterator besitzt, die einen sogenannten Iterator zurückgibt. Der Iterator ist ein Objekt, das über seine Methode next() Werte zurückgibt. Wir sagen: Er iteriert über die Elemente (den Inhalt) des Iterierbaren, einen pro Methodenaufruf.Schauen wir uns an, wie der Konsum für ein Array arr aussieht. Zuerst erstellen Sie einen Iterator über die Methode mit dem Schlüssel Symbol.iterator
> const arr = ['a', 'b', 'c'];
> const iter = arr[Symbol.iterator]();
Dann rufen Sie die Methode next() des Iterators wiederholt auf, um die Elemente "im Inneren" des Arrays abzurufen
> iter.next()
{ value: 'a', done: false }
> iter.next()
{ value: 'b', done: false }
> iter.next()
{ value: 'c', done: false }
> iter.next()
{ value: undefined, done: true }
Wie Sie sehen, gibt next() jedes Element eingepackt in ein Objekt zurück, als Wert der Eigenschaft value. Die boolesche Eigenschaft done gibt an, wann das Ende der Elementsequenz erreicht ist.
Iterable und Iteratoren sind Teil eines sogenannten Protokolls (Schnittstellen plus Regeln für deren Verwendung) für die Iteration. Ein Hauptmerkmal dieses Protokolls ist, dass es sequentiell ist: Der Iterator gibt die Werte einzeln zurück. Das bedeutet, dass eine Iteration eine nicht-lineare Datenstruktur (wie einen Baum) linearisieren wird.
Ich werde die for-of-Schleife (siehe Kap. „Die for-of-Schleife“) verwenden, um verschiedene Arten von iterierbaren Daten zu durchlaufen.
Arrays (und Typed Arrays) sind iterierbar über ihre Elemente.
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'
Strings sind iterierbar, aber sie iterieren über Unicode-Codepunkte, von denen jeder aus einem oder zwei JavaScript-Zeichen bestehen kann.
for (const x of 'a\uD83D\uDC0A') {
console.log(x);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)
Maps sind iterierbar über ihre Einträge. Jeder Eintrag ist als [Schlüssel, Wert]-Paar kodiert, ein Array mit zwei Elementen. Die Einträge werden immer deterministisch iteriert, in der gleichen Reihenfolge, in der sie zur Map hinzugefügt wurden.
const map = new Map().set('a', 1).set('b', 2);
for (const pair of map) {
console.log(pair);
}
// Output:
// ['a', 1]
// ['b', 2]
Beachten Sie, dass WeakMaps nicht iterierbar sind.
Sets sind iterierbar über ihre Elemente (die in der gleichen Reihenfolge iteriert werden, in der sie zum Set hinzugefügt wurden).
const set = new Set().add('a').add('b');
for (const x of set) {
console.log(x);
}
// Output:
// 'a'
// 'b'
Beachten Sie, dass WeakSets nicht iterierbar sind.
arguments Obwohl die spezielle Variable arguments in ECMAScript 6 mehr oder weniger veraltet ist (wegen der Rest-Parameter), ist sie iterierbar.
function printArgs() {
for (const x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// Output:
// 'a'
// 'b'
Die meisten DOM-Datenstrukturen werden irgendwann iterierbar sein.
for (const node of document.querySelectorAll('div')) {
···
}
Beachten Sie, dass die Implementierung dieser Funktionalität noch in Arbeit ist. Aber es ist relativ einfach zu tun, da das Symbol Symbol.iterator nicht mit vorhandenen Eigenschaftsschlüsseln kollidieren kann.
Nicht alle iterierbaren Inhalte müssen aus Datenstrukturen stammen, sie können auch "on the fly" berechnet werden. Zum Beispiel haben alle wichtigen ES6-Datenstrukturen (Arrays, Typed Arrays, Maps, Sets) drei Methoden, die iterierbare Objekte zurückgeben
entries() gibt ein iterierbares Objekt über Einträge zurück, die als [Schlüssel, Wert]-Arrays kodiert sind. Bei Arrays sind die Werte die Array-Elemente und die Schlüssel sind ihre Indizes. Bei Sets sind Schlüssel und Wert gleich – das Set-Element.keys() gibt ein iterierbares Objekt über die Schlüssel der Einträge zurück.values() gibt ein iterierbares Objekt über die Werte der Einträge zurück.Schauen wir uns das an. entries() bietet eine gute Möglichkeit, sowohl Array-Elemente als auch deren Indizes zu erhalten.
const arr = ['a', 'b', 'c'];
for (const pair of arr.entries()) {
console.log(pair);
}
// Output:
// [0, 'a']
// [1, 'b']
// [2, 'c']
Einfache Objekte (wie sie durch Objektliterale erzeugt werden) sind nicht iterierbar.
for (const x of {}) { // TypeError
console.log(x);
}
Warum sind Objekte standardmäßig nicht über ihre Eigenschaften iterierbar? Die Begründung ist folgende. Es gibt zwei Ebenen, auf denen Sie in JavaScript iterieren können
Die Standardisierung der Iteration über Eigenschaften würde diese Ebenen vermischen, was zwei Nachteile hätte
Wenn Engines die Iterierbarkeit über eine Methode Object.prototype[Symbol.iterator]() implementieren würden, gäbe es ein zusätzliches Manko: Objekte, die über Object.create(null) erstellt wurden, wären nicht iterierbar, da Object.prototype nicht in ihrer Prototypenkette enthalten ist.
Es ist wichtig zu bedenken, dass das Iterieren über die Eigenschaften eines Objekts hauptsächlich interessant ist, wenn Sie Objekte als Maps verwenden1. Aber das tun wir nur in ES5, weil wir keine bessere Alternative haben. In ECMAScript 6 haben wir die eingebaute Datenstruktur Map.
Der richtige (und sichere) Weg, über Eigenschaften zu iterieren, ist über eine Hilfsfunktion. Zum Beispiel über objectEntries(), deren Implementierung später gezeigt wird (zukünftige ECMAScript-Versionen könnten etwas Ähnliches eingebaut haben).
const obj = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
Die folgenden ES6-Sprachkonstrukte nutzen das Iterationsprotokoll.
for-of-SchleifeArray.from()...)Promise.all(), Promise.race()yield*Die nächsten Abschnitte beschreiben jedes davon im Detail.
Die Dekonstruktion über Array-Muster funktioniert für jedes iterierbare Objekt.
const set = new Set().add('a').add('b').add('c');
const [x,y] = set;
// x='a'; y='b'
const [first, ...rest] = set;
// first='a'; rest=['b','c'];
for-of-Schleife for-of ist eine neue Schleife in ECMAScript 6. Ihre Grundform sieht so aus:
for (const x of iterable) {
···
}
Weitere Informationen finden Sie in Kap. „Die for-of-Schleife“.
Beachten Sie, dass die Iterierbarkeit von iterable erforderlich ist, sonst kann for-of nicht über einen Wert iterieren. Das bedeutet, dass nicht iterierbare Werte in etwas Iterierbares umgewandelt werden müssen. Zum Beispiel über Array.from().
Array.from() Array.from() konvertiert iterierbare und Array-ähnliche Werte in Arrays. Es ist auch für Typed Arrays verfügbar.
> Array.from(new Map().set(false, 'no').set(true, 'yes'))
[[false,'no'], [true,'yes']]
> Array.from({ length: 2, 0: 'hello', 1: 'world' })
['hello', 'world']
Weitere Informationen zu Array.from() finden Sie in dem Kapitel über Arrays.
...) Der Spread-Operator fügt die Werte eines Iterierbaren in ein Array ein.
> const arr = ['b', 'c'];
> ['a', ...arr, 'd']
['a', 'b', 'c', 'd']
Das bedeutet, dass er Ihnen eine kompakte Möglichkeit bietet, jedes Iterierbare in ein Array zu konvertieren.
const arr = [...iterable];
Der Spread-Operator wandelt auch ein Iterierbares in die Argumente eines Funktions-, Methoden- oder Konstruktoraufrufs um.
> Math.max(...[-1, 8, 3])
8
Der Konstruktor einer Map wandelt ein Iterierbares von [Schlüssel, Wert]-Paaren in eine Map um.
> const map = new Map([['uno', 'one'], ['dos', 'two']]);
> map.get('uno')
'one'
> map.get('dos')
'two'
Der Konstruktor eines Sets wandelt ein Iterierbares von Elementen in ein Set um.
> const set = new Set(['red', 'green', 'blue']);
> set.has('red')
true
> set.has('yellow')
false
Die Konstruktoren von WeakMap und WeakSet funktionieren ähnlich. Außerdem sind Maps und Sets selbst iterierbar (WeakMaps und WeakSets nicht), was bedeutet, dass Sie ihre Konstruktoren zum Klonen verwenden können.
Promise.all() und Promise.race() akzeptieren Iterierbare von Promises.
Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);
yield* yield* ist ein Operator, der nur innerhalb von Generatoren verfügbar ist. Er gibt alle Elemente aus, die von einem Iterierbaren durchlaufen werden.
function* yieldAllValuesOf(iterable) {
yield* iterable;
}
Der wichtigste Anwendungsfall für yield* ist das rekursive Aufrufen eines Generators (der etwas Iterierbares erzeugt).
In diesem Abschnitt erkläre ich im Detail, wie man Iterierbare implementiert. Beachten Sie, dass ES6-Generatoren für diese Aufgabe in der Regel wesentlich bequemer sind als die manuelle Implementierung.
Das Iterationsprotokoll sieht wie folgt aus.
Ein Objekt wird iterierbar (implementiert die Schnittstelle Iterable), wenn es eine Methode (eigen oder geerbt) mit dem Schlüssel Symbol.iterator besitzt. Diese Methode muss einen Iterator zurückgeben, ein Objekt, das über seine Methode next() die Elemente "im Inneren" des Iterierbaren durchläuft.
In TypeScript-Notation sehen die Schnittstellen für Iterierbare und Iteratoren wie folgt aus2.
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return?(value? : any) : IteratorResult;
}
interface IteratorResult {
value: any;
done: boolean;
}
return() ist eine optionale Methode, auf die wir später zurückkommen werden3. Lassen Sie uns zuerst ein Dummy-Iterierbares implementieren, um ein Gefühl dafür zu bekommen, wie Iteration funktioniert.
const iterable = {
[Symbol.iterator]() {
let step = 0;
const iterator = {
next() {
if (step <= 2) {
step++;
}
switch (step) {
case 1:
return { value: 'hello', done: false };
case 2:
return { value: 'world', done: false };
default:
return { value: undefined, done: true };
}
}
};
return iterator;
}
};
Lassen Sie uns überprüfen, ob iterable tatsächlich iterierbar ist.
for (const x of iterable) {
console.log(x);
}
// Output:
// hello
// world
Der Code führt drei Schritte aus, wobei der Zähler step sicherstellt, dass alles in der richtigen Reihenfolge geschieht. Zuerst geben wir den Wert 'hello' zurück, dann den Wert 'world' und dann zeigen wir an, dass das Ende der Iteration erreicht ist. Jedes Element ist in ein Objekt mit den Eigenschaften verpackt
value, das das tatsächliche Element enthält, unddone, ein boolesches Flag, das anzeigt, ob das Ende bereits erreicht wurde.Sie können done weglassen, wenn es false ist, und value, wenn es undefined ist. Das heißt, die switch-Anweisung könnte wie folgt geschrieben werden:
switch (step) {
case 1:
return { value: 'hello' };
case 2:
return { value: 'world' };
default:
return { done: true };
}
Wie im Kapitel über Generatoren (Kapitel über Generatoren) erklärt, gibt es Fälle, in denen Sie auch das letzte Element mit done: true mit einem value wünschen. Andernfalls könnte next() einfacher sein und Elemente direkt zurückgeben (ohne sie in Objekte zu verpacken). Das Ende der Iteration würde dann durch einen speziellen Wert (z. B. ein Symbol) angezeigt.
Schauen wir uns eine weitere Implementierung eines Iterierbaren an. Die Funktion iterateOver() gibt ein Iterierbares über die an sie übergebenen Argumente zurück.
function iterateOver(...args) {
let index = 0;
const iterable = {
[Symbol.iterator]() {
const iterator = {
next() {
if (index < args.length) {
return { value: args[index++] };
} else {
return { done: true };
}
}
};
return iterator;
}
}
return iterable;
}
// Using `iterateOver()`:
for (const x of iterateOver('fee', 'fi', 'fo', 'fum')) {
console.log(x);
}
// Output:
// fee
// fi
// fo
// fum
Die vorherige Funktion kann vereinfacht werden, wenn das Iterierbare und der Iterator dasselbe Objekt sind.
function iterateOver(...args) {
let index = 0;
const iterable = {
[Symbol.iterator]() {
return this;
},
next() {
if (index < args.length) {
return { value: args[index++] };
} else {
return { done: true };
}
},
};
return iterable;
}
Auch wenn das ursprüngliche Iterierbare und der Iterator nicht dasselbe Objekt sind, ist es gelegentlich nützlich, wenn ein Iterator die folgende Methode besitzt (die ihn auch zu einem Iterierbaren macht):
[Symbol.iterator]() {
return this;
}
Alle integrierten ES6-Iteratoren folgen diesem Muster (über einen gemeinsamen Prototyp, siehe Kapitel über Generatoren). Zum Beispiel der Standard-Iterator für Arrays.
> const arr = [];
> const iterator = arr[Symbol.iterator]();
> iterator[Symbol.iterator]() === iterator
true
Warum ist es nützlich, wenn ein Iterator auch iterierbar ist? for-of funktioniert nur für Iterierbare, nicht für Iteratoren. Da Array-Iteratoren iterierbar sind, können Sie eine Iteration in einer anderen Schleife fortsetzen.
const arr = ['a', 'b'];
const iterator = arr[Symbol.iterator]();
for (const x of iterator) {
console.log(x); // a
break;
}
// Continue with same iterator:
for (const x of iterator) {
console.log(x); // b
}
Ein Anwendungsfall für die Fortsetzung einer Iteration ist, dass Sie anfängliche Elemente (z. B. einen Header) entfernen können, bevor Sie den eigentlichen Inhalt über for-of verarbeiten.
return() und throw() Zwei Iterator-Methoden sind optional.
return() gibt einem Iterator die Möglichkeit, aufzuräumen, wenn eine Iteration vorzeitig endet.throw() befasst sich mit der Weiterleitung eines Methodenaufrufs an einen Generator, der über yield* iteriert wird. Er wird im Kapitel über Generatoren erklärt.return() Wie bereits erwähnt, dient die optionale Iterator-Methode return() dazu, einem Iterator die Möglichkeit zu geben, aufzuräumen, wenn er nicht bis zum Ende iteriert wurde. Sie schließt einen Iterator. In for-of-Schleifen kann eine vorzeitige (oder abrupte, im Spezifikations-Sprachgebrauch) Beendigung verursacht werden durch
breakcontinue (wenn Sie eine äußere Schleife fortsetzen, verhält sich continue wie ein break)throwreturnIn jedem dieser Fälle lässt for-of den Iterator wissen, dass die Schleife nicht abgeschlossen wird. Schauen wir uns ein Beispiel an, eine Funktion readLinesSync, die ein Iterierbares von Textzeilen in einer Datei zurückgibt und diese Datei unabhängig davon schließen möchte, was passiert.
function readLinesSync(fileName) {
const file = ···;
return {
···
next() {
if (file.isAtEndOfFile()) {
file.close();
return { done: true };
}
···
},
return() {
file.close();
return { done: true };
},
};
}
Dank return() wird die Datei in der folgenden Schleife ordnungsgemäß geschlossen.
// Only print first line
for (const line of readLinesSync(fileName)) {
console.log(x);
break;
}
Die Methode return() muss ein Objekt zurückgeben. Das liegt daran, wie Generatoren mit der return-Anweisung umgehen und wird im Kapitel über Generatoren erklärt.
Die folgenden Konstrukte schließen Iteratoren, die nicht vollständig "ausgeschöpft" sind:
for-ofyield*Array.from()Map(), Set(), WeakMap(), WeakSet()Promise.all(), Promise.race()Ein späterer Abschnitt enthält weitere Informationen zum Schließen von Iteratoren.
In diesem Abschnitt betrachten wir einige weitere Beispiele für Iterierbare. Die meisten dieser Iterierbaren sind mit Generatoren einfacher zu implementieren. Das Kapitel über Generatoren zeigt, wie.
Hilfsfunktionen und Methoden, die Iterierbare zurückgeben, sind ebenso wichtig wie iterierbare Datenstrukturen. Das Folgende ist eine Hilfsfunktion zum Iterieren über die eigenen Eigenschaften eines Objekts.
function objectEntries(obj) {
let index = 0;
// In ES6, you can use strings or symbols as property keys,
// Reflect.ownKeys() retrieves both
const propKeys = Reflect.ownKeys(obj);
return {
[Symbol.iterator]() {
return this;
},
next() {
if (index < propKeys.length) {
const key = propKeys[index];
index++;
return { value: [key, obj[key]] };
} else {
return { done: true };
}
}
};
}
const obj = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
Eine andere Möglichkeit ist die Verwendung eines Iterators anstelle eines Index, um das Array mit den Eigenschaftsschlüsseln zu durchlaufen.
function objectEntries(obj) {
let iter = Reflect.ownKeys(obj)[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
let { done, value: key } = iter.next();
if (done) {
return { done: true };
}
return { value: [key, obj[key]] };
}
};
}
Kombinatoren4 sind Funktionen, die vorhandene Iterierbare kombinieren, um neue zu erstellen.
take(n, iterable) Beginnen wir mit der Kombinatorfunktion take(n, iterable), die ein Iterierbares über die ersten n Elemente von iterable zurückgibt.
function take(n, iterable) {
const iter = iterable[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
if (n > 0) {
n--;
return iter.next();
} else {
return { done: true };
}
}
};
}
const arr = ['a', 'b', 'c', 'd'];
for (const x of take(2, arr)) {
console.log(x);
}
// Output:
// a
// b
zip(...iterables) zip wandelt n Iterierbare in ein Iterierbares von n-Tupeln um (kodiert als Arrays der Länge n).
function zip(...iterables) {
const iterators = iterables.map(i => i[Symbol.iterator]());
let done = false;
return {
[Symbol.iterator]() {
return this;
},
next() {
if (!done) {
const items = iterators.map(i => i.next());
done = items.some(item => item.done);
if (!done) {
return { value: items.map(i => i.value) };
}
// Done for the first time: close all iterators
for (const iterator of iterators) {
if (typeof iterator.return === 'function') {
iterator.return();
}
}
}
// We are done
return { done: true };
}
}
}
Wie Sie sehen, bestimmt das kürzeste Iterierbare die Länge des Ergebnisses.
const zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']);
for (const x of zipped) {
console.log(x);
}
// Output:
// ['a', 'd']
// ['b', 'e']
// ['c', 'f']
Einige Iterierbare können niemals done sein.
function naturalNumbers() {
let n = 0;
return {
[Symbol.iterator]() {
return this;
},
next() {
return { value: n++ };
}
}
}
Bei einem unendlichen Iterierbaren dürfen Sie nicht "alles" davon iterieren. Zum Beispiel, indem Sie aus einer for-of-Schleife ausbrechen.
for (const x of naturalNumbers()) {
if (x > 2) break;
console.log(x);
}
Oder indem Sie nur den Anfang eines unendlichen Iterierbaren abrufen.
const [a, b, c] = naturalNumbers();
// a=0; b=1; c=2;
Oder indem Sie einen Kombinator verwenden. take() ist eine Möglichkeit.
for (const x of take(3, naturalNumbers())) {
console.log(x);
}
// Output:
// 0
// 1
// 2
Die "Länge" des von zip() zurückgegebenen Iterierbaren wird durch sein kürzestes Eingabe-Iterierbares bestimmt. Das bedeutet, dass zip() und naturalNumbers() Ihnen die Mittel geben, Iterierbare beliebiger (endlicher) Länge zu nummerieren.
const zipped = zip(['a', 'b', 'c'], naturalNumbers());
for (const x of zipped) {
console.log(x);
}
// Output:
// ['a', 0]
// ['b', 1]
// ['c', 2]
Sie mögen sich Sorgen machen, dass das Iterationsprotokoll langsam ist, da für jeden Aufruf von next() ein neues Objekt erstellt wird. Allerdings ist die Speicherverwaltung für kleine Objekte in modernen Engines schnell, und auf lange Sicht können Engines die Iteration so optimieren, dass keine Zwischenobjekte zugewiesen werden müssen. Ein Thread auf es-discuss enthält weitere Informationen.
Grundsätzlich hindert nichts einen Iterator daran, dasselbe Iterationsergebnisobjekt mehrmals wiederzuverwenden – ich erwarte, dass die meisten Dinge gut funktionieren. Es wird jedoch Probleme geben, wenn ein Client Iterationsergebnisse zwischenspeichert.
const iterationResults = [];
const iterator = iterable[Symbol.iterator]();
let iterationResult;
while (!(iterationResult = iterator.next()).done) {
iterationResults.push(iterationResult);
}
Wenn ein Iterator sein Iterationsergebnisobjekt wiederverwendet, enthält iterationResults im Allgemeinen dasselbe Objekt mehrmals.
Sie fragen sich vielleicht, warum ECMAScript 6 keine kombinierten Iterierbaren hat, also Werkzeuge zum Arbeiten mit Iterierbaren oder zum Erstellen von Iterierbaren. Das liegt daran, dass die Pläne sind, in zwei Schritten vorzugehen:
Schließlich wird eine solche Bibliothek oder Teile davon in die Standardbibliothek von JavaScript aufgenommen.
Wenn Sie einen Eindruck davon bekommen möchten, wie eine solche Bibliothek aussehen könnte, werfen Sie einen Blick auf das Standard-Python-Modul itertools.
Ja, Iterierbare sind schwierig zu implementieren – wenn man sie manuell implementiert. Das nächste Kapitel stellt Generatoren vor, die bei dieser Aufgabe (unter anderem) helfen.
Das Iterationsprotokoll umfasst die folgenden Schnittstellen (throw() aus Iterator habe ich weggelassen, da es nur von yield* unterstützt wird und dort optional ist).
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return?(value? : any) : IteratorResult;
}
interface IteratorResult {
value : any;
done : boolean;
}
Regeln für next()
x zu produzieren hat, gibt next() Objekte { value: x, done: false } zurück.next() immer ein Objekt zurückgeben, dessen Eigenschaft done true ist.IteratorResult Die Eigenschaft done eines Iteratorergebnisses muss nicht true oder false sein, wahr oder falsch reicht aus. Alle integrierten Sprachmechanismen lassen Sie done: false weglassen.
Einige Iterierbare erzeugen bei jeder Anfrage einen neuen Iterator. Zum Beispiel Arrays.
function getIterator(iterable) {
return iterable[Symbol.iterator]();
}
const iterable = ['a', 'b'];
console.log(getIterator(iterable) === getIterator(iterable)); // false
Andere Iterierbare geben jedes Mal denselben Iterator zurück. Zum Beispiel Generatorobjekte.
function* elements() {
yield 'a';
yield 'b';
}
const iterable = elements();
console.log(getIterator(iterable) === getIterator(iterable)); // true
Ob ein Iterierbares frische Iteratoren erzeugt oder nicht, ist wichtig, wenn Sie dasselbe Iterierbare mehrmals durchlaufen. Zum Beispiel über die folgende Funktion.
function iterateTwice(iterable) {
for (const x of iterable) {
console.log(x);
}
for (const x of iterable) {
console.log(x);
}
}
Mit frischen Iteratoren können Sie dasselbe Iterierbare mehrmals durchlaufen.
iterateTwice(['a', 'b']);
// Output:
// a
// b
// a
// b
Wenn jedes Mal derselbe Iterator zurückgegeben wird, können Sie das nicht.
iterateTwice(elements());
// Output:
// a
// b
Beachten Sie, dass jeder Iterator in der Standardbibliothek auch ein Iterierbares ist. Seine Methode [Symbol.iterator]() gibt this zurück, was bedeutet, dass sie immer denselben Iterator (sich selbst) zurückgibt.
Das Iterationsprotokoll unterscheidet zwei Arten, einen Iterator zu beenden:
next() auf, bis er ein Objekt zurückgibt, dessen Eigenschaft done true ist.return() teilt man dem Iterator mit, dass man next() nicht mehr aufrufen wird.Regeln für den Aufruf von return()
return() ist eine optionale Methode, nicht alle Iteratoren haben sie. Iteratoren, die sie haben, werden als schließbar bezeichnet.return() sollte nur aufgerufen werden, wenn ein Iterator nicht erschöpft wurde. Zum Beispiel ruft for-of return() auf, wenn es "abrupt" verlassen wird (bevor es abgeschlossen ist). Die folgenden Operationen verursachen abrupte Ausstiege: break, continue (mit einem Label eines äußeren Blocks), return, throw.Regeln für die Implementierung von return()
return(x) sollte normalerweise das Objekt { done: true, value: x } erzeugen, aber Sprachmechanismen werfen nur einen Fehler (Quelle in Spezifikation), wenn das Ergebnis kein Objekt ist.return() aufgerufen wurde, sollten die von next() zurückgegebenen Objekte ebenfalls done sein.Der folgende Code veranschaulicht, dass die for-of-Schleife return() aufruft, wenn sie vor dem Erhalt eines done-Iterator-Ergebnisses abgebrochen wird. Das heißt, return() wird auch aufgerufen, wenn Sie nach Erhalt des letzten Werts abbrechen. Das ist subtil und man muss vorsichtig sein, um es richtig zu machen, wenn man manuell iteriert oder Iteratoren implementiert.
function createIterable() {
let done = false;
const iterable = {
[Symbol.iterator]() {
return this;
},
next() {
if (!done) {
done = true;
return { done: false, value: 'a' };
} else {
return { done: true, value: undefined };
}
},
return() {
console.log('return() was called!');
},
};
return iterable;
}
for (const x of createIterable()) {
console.log(x);
// There is only one value in the iterable and
// we abort the loop after receiving it
break;
}
// Output:
// a
// return() was called!
Ein Iterator ist schließbar, wenn er eine Methode return() besitzt. Nicht alle Iteratoren sind schließbar. Zum Beispiel sind Array-Iteratoren nicht schließbar.
> let iterable = ['a', 'b', 'c'];
> const iterator = iterable[Symbol.iterator]();
> 'return' in iterator
false
Generatorobjekte sind standardmäßig schließbar. Zum Beispiel die, die von der folgenden Generatorfunktion zurückgegeben werden.
function* elements() {
yield 'a';
yield 'b';
yield 'c';
}
Wenn Sie return() auf das Ergebnis von elements() aufrufen, wird die Iteration beendet.
> const iterator = elements();
> iterator.next()
{ value: 'a', done: false }
> iterator.return()
{ value: undefined, done: true }
> iterator.next()
{ value: undefined, done: true }
Wenn ein Iterator nicht schließbar ist, können Sie ihn nach einem abrupten Ausstieg (wie dem in Zeile A) aus einer for-of-Schleife weiter durchlaufen.
function twoLoops(iterator) {
for (const x of iterator) {
console.log(x);
break; // (A)
}
for (const x of iterator) {
console.log(x);
}
}
function getIterator(iterable) {
return iterable[Symbol.iterator]();
}
twoLoops(getIterator(['a', 'b', 'c']));
// Output:
// a
// b
// c
Umgekehrt gibt elements() einen schließbaren Iterator zurück und die zweite Schleife in twoLoops() hat nichts zu durchlaufen.
twoLoops(elements());
// Output:
// a
Die folgende Klasse ist eine generische Lösung, um zu verhindern, dass Iteratoren geschlossen werden. Sie tut dies, indem sie den Iterator umschließt und alle Methodenaufrufe weiterleitet, außer return().
class PreventReturn {
constructor(iterator) {
this.iterator = iterator;
}
/** Must also be iterable, so that for-of works */
[Symbol.iterator]() {
return this;
}
next() {
return this.iterator.next();
}
return(value = undefined) {
return { done: false, value };
}
// Not relevant for iterators: `throw()`
}
Wenn wir PreventReturn verwenden, wird das Ergebnis des Generators elements() nach dem abrupten Ausstieg in der ersten Schleife von twoLoops() nicht geschlossen.
function* elements() {
yield 'a';
yield 'b';
yield 'c';
}
function twoLoops(iterator) {
for (const x of iterator) {
console.log(x);
break; // abrupt exit
}
for (const x of iterator) {
console.log(x);
}
}
twoLoops(elements());
// Output:
// a
twoLoops(new PreventReturn(elements()));
// Output:
// a
// b
// c
Es gibt noch eine weitere Möglichkeit, Generatoren nicht schließbar zu machen: Alle Generatorobjekte, die von der Generatorfunktion elements() erzeugt werden, haben das Prototypobjekt elements.prototype. Über elements.prototype können Sie die Standardimplementierung von return() (die sich im Prototyp von elements.prototype befindet) wie folgt ausblenden.
// Make generator object unclosable
// Warning: may not work in transpilers
elements.prototype.return = undefined;
twoLoops(elements());
// Output:
// a
// b
// c
try-finally Einige Generatoren müssen nach Abschluss der Iteration über sie aufräumen (zugewiesene Ressourcen freigeben, Dateien schließen usw.). Naiv sieht die Implementierung so aus:
function* genFunc() {
yield 'a';
yield 'b';
console.log('Performing cleanup');
}
In einer normalen for-of-Schleife ist alles in Ordnung.
for (const x of genFunc()) {
console.log(x);
}
// Output:
// a
// b
// Performing cleanup
Wenn Sie jedoch die Schleife nach dem ersten yield verlassen, scheint die Ausführung dort für immer anzuhalten und erreicht nie den Bereinigungsschritt.
for (const x of genFunc()) {
console.log(x);
break;
}
// Output:
// a
Was tatsächlich passiert, ist, dass for-of jedes Mal, wenn man eine for-of-Schleife frühzeitig verlässt, ein return() an den aktuellen Iterator sendet. Das bedeutet, dass der Bereinigungsschritt nicht erreicht wird, weil die Generatorfunktion vorher zurückkehrt.
Glücklicherweise ist dies leicht zu beheben, indem die Bereinigung in einer finally-Klausel durchgeführt wird.
function* genFunc() {
try {
yield 'a';
yield 'b';
} finally {
console.log('Performing cleanup');
}
}
Jetzt funktioniert alles wie gewünscht.
for (const x of genFunc()) {
console.log(x);
break;
}
// Output:
// a
// Performing cleanup
Das allgemeine Muster für die Verwendung von Ressourcen, die geschlossen oder auf irgendeine Weise bereinigt werden müssen, ist daher:
function* funcThatUsesResource() {
const resource = allocateResource();
try {
···
} finally {
resource.deallocate();
}
}
const iterable = {
[Symbol.iterator]() {
function hasNextValue() { ··· }
function getNextValue() { ··· }
function cleanUp() { ··· }
let returnedDoneResult = false;
return {
next() {
if (hasNextValue()) {
const value = getNextValue();
return { done: false, value: value };
} else {
if (!returnedDoneResult) {
// Client receives first `done` iterator result
// => won’t call `return()`
cleanUp();
returnedDoneResult = true;
}
return { done: true, value: undefined };
}
},
return() {
cleanUp();
}
};
}
}
Beachten Sie, dass Sie cleanUp() aufrufen müssen, wenn Sie zum ersten Mal ein done-Iterator-Ergebnis zurückgeben. Sie dürfen dies nicht früher tun, da dann möglicherweise noch return() aufgerufen wird. Das kann knifflig sein.
Wenn Sie Iteratoren verwenden, sollten Sie sie ordnungsgemäß schließen. In Generatoren können Sie for-of die ganze Arbeit für Sie erledigen lassen.
/**
* Converts a (potentially infinite) sequence of
* iterated values into a sequence of length `n`
*/
function* take(n, iterable) {
for (const x of iterable) {
if (n <= 0) {
break; // closes iterable
}
n--;
yield x;
}
}
Wenn Sie die Dinge manuell verwalten, ist mehr Arbeit erforderlich.
function* take(n, iterable) {
const iterator = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iterator.next();
if (done) break; // exhausted
if (n <= 0) {
// Abrupt exit
maybeCloseIterator(iterator);
break;
}
yield value;
n--;
}
}
function maybeCloseIterator(iterator) {
if (typeof iterator.return === 'function') {
iterator.return();
}
}
Noch mehr Arbeit ist erforderlich, wenn Sie keine Generatoren verwenden.
function take(n, iterable) {
const iter = iterable[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
if (n > 0) {
n--;
return iter.next();
} else {
maybeCloseIterator(iter);
return { done: true };
}
},
return() {
n = 0;
maybeCloseIterator(iter);
}
};
}
return() aufgerufen wird.try-finally die Behandlung beider Fälle an einem Ort.return() geschlossen wurde, sollte er keine weiteren Iterator-Ergebnisse über next() mehr liefern.for-of usw.)return zu schließen, wenn – und nur wenn – Sie ihn nicht erschöpfen. Dies richtig hinzubekommen, kann knifflig sein.