JavaScript Iterables und Iteratoren
Iterables sind ein grundlegendes Konzept in JavaScript. Dieser Leitfaden erklärt Iterables, Iteratoren und praktische Techniken für Sammlungen.
Einführung in JavaScript Iterables
JavaScript Iterables sind Objekte, die ein bestimmtes Protokoll implementieren und dadurch von Iterations-Konstrukten wie for...of verarbeitet werden können. Dieser Leitfaden behandelt die grundlegenden Konzepte, eingebaute Iterables und praktische Techniken für die Arbeit mit Sammlungen.
Was sind Iterables in JavaScript?
Im Kern ist ein Iterable ein Objekt, das die Methode Symbol.iterator implementiert und damit sequenziellen Zugriff auf seine Elemente ermöglicht. Mehrere eingebaute Typen in JavaScript sind iterierbar, darunter Array, String, Map, Set und weitere. Es ist wichtig, zwischen einem Iterable (dem Objekt, das durchlaufen wird) und einem Iterator (dem von Symbol.iterator zurückgegebenen Objekt, das den Durchlauf tatsächlich ausführt) zu unterscheiden. Diese Iterables sind integraler Bestandteil verschiedener Operationen wie Schleifen und Datenmanipulation.
Für eine detaillierte Erkundung der JavaScript Map- und Set-Objekte verweisen wir auf unseren umfassenden Leitfaden zu JavaScript Map und Set.
Beispiel eines Iterable: Array
Schauen wir uns ein grundlegendes Beispiel eines Iterable in JavaScript an:
Dieses Code-Snippet zeigt das Iterieren über ein array von Früchten, ein gängiges Iterable. Die for...of-Schleife ruft automatisch die Symbol.iterator-Methode des Iterable auf und verbraucht den resultierenden Iterator, bis done den Wert true hat. Siehe JavaScript-Schleifen für die vollständige Familie der Schleifenkonstrukte.
Verwechseln Sie nicht for...of mit for...in. for...of iteriert über die Werte, die ein Iterable produziert (Array-Elemente, String-Zeichen, Map-Einträge). for...in iteriert über die aufzählbaren Eigenschaftsschlüssel eines Objekts – einschließlich geerbter – und ist für einfache Objekte gedacht, nicht für Arrays. Die Verwendung von for...in auf einem array liefert Index-Strings ("0", "1", …) und kann zusätzliche Eigenschaften erfassen. Bevorzugen Sie daher for...of für geordnete Daten.
Das Iterator-Protokoll
Das Herzstück eines Iterable ist seine Symbol.iterator-Methode. Das Protokoll ist ein präziser Vertrag:
- Ein Iterable hat eine Methode mit dem Schlüssel
Symbol.iterator. Der Aufruf dieser Methode gibt einen Iterator zurück. - Ein Iterator ist ein Objekt mit einer
next()-Methode. - Jeder Aufruf von
next()gibt ein Objekt{ value, done }zurück:done: false—valueist das nächste Element in der Sequenz.done: true— die Iteration ist abgeschlossen (valuewird dann ignoriert oder trägt ein optionales Endergebnis).
Jedes Objekt, das diesen Vertrag einhält, funktioniert mit for...of, dem Spread-Operator, Destructuring und Array.from() – auch wenn Sie das Objekt selbst geschrieben haben. Symbol ist ein eingebauter eindeutiger Schlüssel; siehe Symbol-Typ, warum diese Protokollmethoden einen solchen statt eines einfachen String-Namens verwenden.
Beispiel: Ein benutzerdefiniertes range-Iterable
Ein klassischer Anwendungsfall ist ein Objekt, das einen numerischen Bereich lazy erzeugt, ohne jemals ein array aufzubauen:
In diesem Beispiel gibt die Methode [Symbol.iterator]() jedes Mal einen neuen Iterator zurück, sodass dasselbe range-Objekt mehrfach durchlaufen werden kann. Die Methodenkurzschreibweise stellt sicher, dass this korrekt auf das range-Objekt verweist. (Die Verwendung einer Pfeilfunktion für [Symbol.iterator] würde this lexikalisch erfassen und das Muster kaputt machen.)
Generatoren: die einfache Methode zum Erstellen von Iterables
Das manuelle Schreiben von next() und das Verwalten des Zustands von Hand ist ausführlich. Eine Generatorfunktion – deklariert mit function* und unter Verwendung von yield – erzeugt automatisch einen Iterator. Jedes yield pausiert die Funktion und übergibt einen Wert an den Verbraucher; die Ausführung wird beim nächsten next()-Aufruf fortgesetzt. Dasselbe range wird damit erheblich kürzer:
Das * vor dem Methodennamen macht sie zu einer Generatormethode, sodass range mit kaum Boilerplate iterierbar ist. Für alles, was Generatoren können – einschließlich bidirektionaler Kommunikation und Delegation mit yield* – siehe Generatoren und Async-Iteratoren und Generatoren.
Unendliche und lazy Sequenzen
Da ein Iterator den nächsten Wert nur auf Anfrage berechnet, kann er Sequenzen beschreiben, die zu groß – oder sogar unendlich – sind, um sie im Speicher zu halten. Das ist der Hauptgrund, einen benutzerdefinierten Iterator statt eines einfachen array zu schreiben:
Verwenden Sie niemals Spread ([...naturals()]) oder for...of ohne break bei einem unendlichen Iterable – das führt zu einer Endlosschleife. Ziehen Sie stattdessen eine begrenzte Anzahl von Werten mit next() heraus.
Iterables verarbeiten
Sobald ein Objekt iterierbar ist, öffnet sich die gesamte Sprache für es: Jedes Konstrukt, das ein Iterable akzeptiert, funktioniert mit Ihrem benutzerdefinierten Typ genauso wie mit Arrays.
Verwendung von Array.from()
Die Methode Array.from() erstellt ein neues array aus einem beliebigen Iterable- (oder array-ähnlichen) Objekt. Sie akzeptiert auch eine optionale Map-Funktion als zweites Argument, die auf jedes Element beim Aufbau des array angewendet wird – praktischer als Array.from(it).map(fn), da ein zweiter Durchlauf entfällt:
Siehe JavaScript Map und Set für mehr zu Set, und Array-Methoden für das, was Sie tun können, sobald Sie ein array haben.
Spread-Syntax mit Iterables
Die Spread-Syntax (...) expandiert ein Iterable überall dort, wo Argumente oder Elemente erwartet werden – zum Zusammenführen von Arrays, Kopieren oder Übergeben von Elementen als Funktionsargumente:
Für das vollständige Bild von ... in Spread- und Collect-Positionen, siehe Rest-Parameter und Spread-Syntax.
Destructuring und Rest
Destructuring-Zuweisung entnimmt Werte aus einem beliebigen Iterable nach Position, und das Rest-Muster (...) fasst den Rest in einem array zusammen:
Mehr dazu erfahren Sie unter Destructuring-Zuweisung.
Das String-Iterable und Unicode-Sicherheit
Strings sind iterierbar, und entscheidend ist, dass der Iterator Unicode-Codepunkte durchläuft, nicht 16-Bit-Codeeinheiten. Das bedeutet, dass Surrogatpaar-Zeichen (Emoji, einige Schriften) intakt bleiben – im Gegensatz zur Indizierung mit [i] oder älteren for-Schleifen über .length, die sie aufteilen können:
Wann immer Sie Zeichen, die dem Nutzer angezeigt werden, korrekt zählen oder aufteilen müssen, iterieren Sie den string (oder spreaden Sie ihn) statt sich auf .length zu verlassen. Weitere Informationen finden Sie im Kapitel Strings.
Zusammenfassung
- Ein Objekt ist iterierbar, wenn es eine
[Symbol.iterator]()-Methode hat, die einen Iterator zurückgibt – ein Objekt, dessennext(){ value, done }liefert. - Verwenden Sie einen Generator (
function*/yield) stattnext()von Hand zu schreiben; das ist die kürzeste und fehlerunanfälligste Methode, etwas iterierbar zu machen. - Verwenden Sie
for...offür die Werte geordneter/iterierbarer Daten; verwenden Siefor...innur für die Schlüssel einfacher Objekte. - Einmal iterierbar, lässt sich Ihr Typ nahtlos mit Spread, Destructuring, Rest,
Array.from(it, mapFn)und vielen eingebauten APIs verwenden. - Iteratoren sind lazy, sodass sie unendliche oder sehr große Sequenzen modellieren können, die in einem array niemals möglich wären.
- Strings iterieren (nicht indizieren), um Unicode-Zeichen wie Emoji sicher zu verarbeiten.