Menu
Im Playground testen

JavaScript Iteratoren & Generatoren: function*, yield

Wie das Iterator-Protokoll in JavaScript funktioniert, wie du eigene Objekte iterierbar machst und warum Generator-Funktionen dir dabei jede Menge Arbeit abnehmen.

Das Iterator-Protokoll

Viele Features in JavaScript – for...of, Spread (...), Destructuring, Array.from, Promise.all – basieren auf demselben Mechanismus: dem Iterator-Protokoll. Sobald du das Prinzip einmal verstanden hast, wirken all diese Features nur noch wie Varianten derselben Idee.

Ein Iterator ist schlicht ein Objekt mit einer next()-Methode, die { value, done } zurückgibt:

index.js
Output
Click Run to see the output here.

Rufe einfach wiederholt next() auf. Jeder Aufruf liefert den nächsten Wert und ein done-Flag zurück. Sobald done auf true steht, ist die Sequenz zu Ende. Mehr steckt nicht hinter dem Protokoll – vier Tastenanschläge und ein Boolean.

Iterable vs. Iterator in JavaScript

Daneben gibt es noch ein zweites, eng verwandtes Konzept: das Iterable. Ein Iterable ist alles, was weiß, wie man einen Iterator erzeugt. Dafür hinterlegt es eine Methode unter einem speziellen Schlüssel: Symbol.iterator.

index.js
Output
Click Run to see the output here.

Arrays sind Iterables. Ein Aufruf von numbers[Symbol.iterator]() liefert dir jedes Mal einen frischen Iterator. Auch Strings, Map, Set und arguments sind Iterables – und genau deshalb funktioniert for...of mit all diesen Typen.

Der Unterschied ist wichtig: Das Iterable ist die Sammlung, der Iterator der Cursor darauf. Von einem Iterable kannst du dir beliebig viele unabhängige Cursor holen.

Warum for...of funktioniert

for...of ist im Grunde nur Syntaxzucker für das Iterator-Protokoll. Intern wird Symbol.iterator aufgerufen und danach so lange next(), bis done den Wert true hat:

index.js
Output
Click Run to see the output here.

Spread und Destructuring machen im Grunde dasselbe – beide laufen einen Iterator durch, bis er fertig ist:

index.js
Output
Click Run to see the output here.

Jedes Objekt, das du selbst baust und das Symbol.iterator implementiert, kann diese Sprachfeatures automatisch mitnutzen.

Eigenes Iterable in JavaScript bauen

Schreiben wir uns ein range-Objekt, das die Zahlen von start bis end liefert:

index.js
Output
Click Run to see the output here.

Ein paar Dinge, die hier auffallen:

  • [Symbol.iterator]() nutzt einen computed method name. Der Schlüssel ist das Symbol selbst, nicht der String "Symbol.iterator".
  • Jeder Aufruf von [Symbol.iterator]() liefert einen frisch erzeugten Iterator mit eigenem current zurück. Genau deshalb kannst du range zweimal durchlaufen, ohne dass er "aufgebraucht" ist.
  • Der zurückgegebene Iterator braucht nur next(). Mehr nicht.

Das funktioniert, ist aber ziemlich umständlich. Es geht deutlich eleganter.

Generatoren kommen ins Spiel

Eine Generatorfunktion wird mit function* deklariert (das Sternchen ist wichtig). Statt die Funktion bis zum Ende durchlaufen zu lassen, kann sie an einem yield-Ausdruck pausieren und später dort weitermachen. Der Aufruf führt den Funktionsrumpf noch nicht aus — du bekommst stattdessen ein Generator-Objekt zurück, das gleichzeitig Iterator und Iterable ist:

index.js
Output
Click Run to see the output here.

Jeder next()-Aufruf führt den Funktionsrumpf aus, bis ein yield erreicht wird, pausiert dann und gibt { value, done: false } zurück. Ist die Funktion durchgelaufen, bekommst du { value: undefined, done: true }.

Und weil Generatoren selbst Iterables sind, funktionieren sie mit allem aus dem vorherigen Abschnitt:

index.js
Output
Click Run to see the output here.

range neu gedacht – diesmal mit einem Generator

Vergleiche mal die ausführliche Variante von oben mit dieser hier:

index.js
Output
Click Run to see the output here.

Das war's. Das * vor [Symbol.iterator] macht aus der Methode einen Generator. yield i ersetzt das komplette selbstgebaute Iterator-Objekt. Kein next, kein done, keine Gefahr von Off-by-one-Fehlern — einfach eine ganz normale Schleife, bei der statt push eben yield steht.

Genau dafür sind Generatoren da. Sie verwandeln "schreib einen Iterator" in "schreib eine Funktion, die yielded".

yield vs. return

yield pausiert, return beendet. Du kannst so oft yielden, wie du willst — der Generator macht danach genau an der Stelle weiter, an der er unterbrochen wurde:

index.js
Output
Click Run to see the output here.

Ein return innerhalb eines Generators erscheint als { value: "done", done: true } beim Aufruf, der ihn beendet. for...of und Spread ignorieren diesen zurückgegebenen Wert — sie verarbeiten nur Elemente, bei denen done den Wert false hat. Verwende also return value nicht, um ein letztes Element in eine Schleife einzuschmuggeln; es wird übersprungen.

Lazy Evaluation und unendliche Sequenzen

Generatoren liefern Werte bedarfsgesteuert, einen nach dem anderen. Dadurch lassen sich Sequenzen abbilden, die als Array gar nicht möglich wären:

index.js
Output
Click Run to see the output here.

Die Schleife ist wortwörtlich while (true), und trotzdem bleibt das Programm nicht hängen – denn ein Generator schreitet nur dann voran, wenn jemand den nächsten Wert anfordert. Du kannst die ersten N Werte abgreifen, aufhören, und der Rest läuft schlichtweg nie:

index.js
Output
Click Run to see the output here.

take ist selbst ein Generator, der einen anderen umhüllt. Genau dieses Verketten von Generatoren macht sie so reizvoll – kleine Bausteine, die jeweils genau eine Aufgabe übernehmen.

Delegieren mit yield*

Wenn ein Generator alle Werte aus einem anderen Iterable durchreichen soll, übernimmt yield* diese Delegation:

index.js
Output
Click Run to see the output here.

yield* funktioniert mit jedem Iterable – Arrays, Sets, anderen Generatoren – und reicht jeden Wert einzeln durch. Quasi das Spread-Äquivalent für Iteratoren.

Async-Generatoren in Kürze

Ein Generator, den du mit async function* deklarierst, darf Werte per yield ausgeben, deren Berechnung Zeit braucht – praktisch, wenn du über eine API streamst oder eine Datei in Chunks einliest. Konsumiert wird er mit for await...of:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item;
    next = page.nextUrl;
  }
}

for await (const item of paginate("/api/users")) {
  console.log(item);
}

Das Snippet lässt sich hier nicht ausführen (es braucht einen echten Endpoint), aber es lohnt sich zu wissen, dass es diese Form gibt. Wenn du reguläre Generatoren verstanden hast, sind Async-Generatoren dieselbe Idee – nur mit ein paar await dazwischen.

Wann lohnt sich ein Generator?

Greif zu einem Generator, wenn:

  • Die Sequenz unendlich ist oder es sein könnte – IDs, Zeitstempel, Retry-Verzögerungen.
  • Alle Werte zu erzeugen teuer ist und der Consumer möglicherweise früher aufhört.
  • Du Symbol.iterator auf einem eigenen Objekt implementierst. Das ist fast immer kürzer, als ein { next() }-Objekt von Hand zu schreiben.
  • Du Streaming-Transformationen (take, filter, map) verketten willst, ohne Zwischen-Arrays anzulegen.

Nimm ein klassisches Array, wenn die Daten ohnehin schon im Speicher liegen und überschaubar sind. Generatoren sind nicht umsonst – die Mechanik, die eine Funktion anhält und wieder fortsetzt, kostet Performance, und Stack Traces durch Generator-Code sind oft schwerer zu lesen.

Weiter geht's: Symbols

Symbol.iterator ist das erste Symbol, dem die meisten begegnen, aber bei Weitem nicht das einzige. Symbols sind ein primitiver Datentyp, der genau für solche Erweiterungspunkte gedacht ist – eindeutige Schlüssel, über die sich die Sprache und dein eigener Code in Objekte einklinken können, ohne mit normalen Property-Namen zu kollidieren. Darum geht's auf der nächsten Seite.

Häufig gestellte Fragen

Was ist der Unterschied zwischen einem Iterable und einem Iterator in JavaScript?

Ein Iterable ist jedes Objekt, das eine Symbol.iterator-Methode besitzt, die einen Iterator zurückgibt. Der Iterator selbst ist das Objekt, das tatsächlich Werte liefert — er hat eine next()-Methode, die { value, done } zurückgibt. Arrays, Strings, Map und Set sind Iterables; wenn du ihre Symbol.iterator-Methode aufrufst, bekommst du einen Iterator, den du Schritt für Schritt durchlaufen kannst.

Was ist eine Generator-Funktion in JavaScript?

Eine Funktion, die mit function* deklariert wird und Werte per yield lazy erzeugt. Der Aufruf führt den Funktionsrumpf noch nicht aus — stattdessen bekommst du ein Generator-Objekt zurück, das gleichzeitig Iterator und Iterable ist. Jeder next()-Aufruf läuft bis zum nächsten yield, pausiert dort und gibt den yieldeten Wert zurück.

Was ist der Unterschied zwischen yield und return in einem Generator?

yield pausiert den Generator und gibt einen Wert nach außen, aber die Funktion kann beim nächsten next()-Aufruf genau dort weitermachen, wo sie stehen geblieben ist. return beendet den Generator endgültig — es setzt done: true, danach kommen keine weiteren Werte mehr. yield kannst du beliebig oft verwenden; ein sinnvolles return gibt's nur einmal.

Wann sollte ich einen Generator statt eines Arrays verwenden?

Immer dann, wenn die Sequenz unendlich ist, teuer zu berechnen ist oder du ohnehin nur einen Teil der Werte brauchst. Ein Generator liefert die Elemente einzeln bei Bedarf, sodass du einen endlosen Strom von IDs oder paginierte API-Ergebnisse abbilden kannst, ohne alles im Voraus im Speicher zu halten. Wenn du dagegen schon ein kleines festes Array hast, nimm einfach das Array.

Lerne mit Coddy zu programmieren

LOS GEHT'S