21. Iterierbare Objekte und Iteratoren
Inhaltsverzeichnis
Bitte unterstützen Sie dieses Buch: kaufen Sie es (PDF, EPUB, MOBI) oder spenden Sie
(Werbung, bitte nicht blockieren.)

21. Iterierbare Objekte und Iteratoren



21.1 Überblick

ES6 führt einen neuen Mechanismus zur Durchquerung von Daten ein: Iteration. Zwei Konzepte sind zentral für die Iteration

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;
}

21.1.1 Iterierbare Werte

Die folgenden Werte sind iterierbar

Einfache Objekte sind nicht iterierbar (warum das so ist, wird in einem eigenen Abschnitt erklärt).

21.1.2 Konstrukte, die Iteration unterstützen

Sprachkonstrukte, die über Iteration auf Daten zugreifen

21.2 Iterierbarkeit

Die Idee der Iterierbarkeit ist folgende.

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.

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.

21.3 Iterierbare Datenquellen

Ich werde die for-of-Schleife (siehe Kap. „Die for-of-Schleife“) verwenden, um verschiedene Arten von iterierbaren Daten zu durchlaufen.

21.3.1 Arrays

Arrays (und Typed Arrays) sind iterierbar über ihre Elemente.

for (const x of ['a', 'b']) {
    console.log(x);
}
// Output:
// 'a'
// 'b'

21.3.2 Strings

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)

21.3.3 Maps

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.

21.3.4 Sets

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.

21.3.5 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'

21.3.6 DOM-Datenstrukturen

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.

21.3.7 Iterierbare berechnete Daten

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

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']

21.3.8 Einfache Objekte sind nicht iterierbar

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

  1. Die Programmebene: Das Iterieren über Eigenschaften bedeutet, die Struktur des Programms zu untersuchen.
  2. Die Datenebene: Das Iterieren über eine Datenstruktur bedeutet, die vom Programm verwalteten Daten zu untersuchen.

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.

21.3.8.1 Wie man über Eigenschaften iteriert

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

21.4 Iterierende Sprachkonstrukte

Die folgenden ES6-Sprachkonstrukte nutzen das Iterationsprotokoll.

Die nächsten Abschnitte beschreiben jedes davon im Detail.

21.4.1 Dekonstruktion über ein Array-Muster

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'];

21.4.2 Die 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().

21.4.3 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.

21.4.4 Der Spread-Operator (...)

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

21.4.5 Maps und Sets

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.

21.4.6 Promises

Promise.all() und Promise.race() akzeptieren Iterierbare von Promises.

Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);

21.4.7 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).

21.5 Implementierung von Iterierbaren

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

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

21.5.1 Iteratoren, die iterierbar sind

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.

21.5.2 Optionale Iterator-Methoden: return() und throw()

Zwei Iterator-Methoden sind optional.

21.5.2.1 Schließen von Iteratoren über 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

In 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:

Ein späterer Abschnitt enthält weitere Informationen zum Schließen von Iteratoren.

21.6 Weitere Beispiele für Iterierbare

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.

21.6.1 Hilfsfunktionen, die Iterierbare zurückgeben

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]] };
        }
    };
}

21.6.2 Kombinatoren für Iterierbare

Kombinatoren4 sind Funktionen, die vorhandene Iterierbare kombinieren, um neue zu erstellen.

21.6.2.1 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
21.6.2.2 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']

21.6.3 Unendliche Iterierbare

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]

21.7 FAQ: Iterierbare Objekte und Iteratoren

21.7.1 Ist das Iterationsprotokoll nicht langsam?

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.

21.7.2 Kann ich dasselbe Objekt mehrmals wiederverwenden?

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.

21.7.3 Warum hat ECMAScript 6 keine kombinierten Iterierbaren?

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.

21.7.4 Sind Iterierbare nicht schwierig zu implementieren?

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.

21.8 Das ECMAScript 6 Iterationsprotokoll im Detail

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;
}

21.8.1 Iteration

Regeln für next()

21.8.1.1 Das 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.

21.8.1.2 Iterierbare Objekte, die frische Iteratoren zurückgeben, im Vergleich zu denen, die immer denselben Iterator zurückgeben

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.

21.8.2 Schließen von Iteratoren

Das Iterationsprotokoll unterscheidet zwei Arten, einen Iterator zu beenden:

Regeln für den Aufruf von return()

Regeln für die Implementierung von return()

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!
21.8.2.1 Schließbare Iteratoren

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
21.8.2.2 Verhindern, dass Iteratoren geschlossen werden

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
21.8.2.3 Behandlung der Bereinigung in Generatoren über 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();
    }
}
21.8.2.4 Behandlung der Bereinigung in manuell implementierten Iteratoren
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.

21.8.2.5 Schließen von Iteratoren, die Sie verwenden

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);
        }
    };
}

21.8.3 Checkliste

Weiter: 22. Generatoren