Lazy Iteration und unendliche Sequenzen
Generator Functions sind eine der unterschätztesten JavaScript-Funktionen: Sie produzieren Werte on-demand, verarbeiten unendliche Datenströme mit konstantem Speicherbedarf und ermöglichen elegante asynchrone Iteration – ohne externe Bibliotheken und ohne Promise-Callback-Chaos.
Inhaltsverzeichnis
- 1. Was Generator Functions grundlegend anders machen
- 2. Das Iterator-Protokoll: next(), return() und throw()
- 3. Lazy Evaluation: Werte nur bei Bedarf berechnen
- 4. Unendliche Sequenzen ohne Speicherprobleme
- 5. yield* — Delegation an andere Iterables
- 6. Bidirektionale Kommunikation mit next(value)
- 7. Async Generators: asynchrone Lazy Iteration
- 8. Generator-Pipelines für Datentransformation
- 9. Generatoren vs. Arrays und Streams im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Generator Functions grundlegend anders machen
Eine gewöhnliche JavaScript-Funktion startet, führt Code aus und gibt einen einzelnen Wert zurück. Eine Generator Function – erkennbar am Sternchen hinter dem function-Keyword – kann ihren Ausführungskontext pausieren und eine Sequenz von Werten nacheinander liefern. Das Schlüsselwort yield pausiert die Ausführung und gibt den aktuellen Wert an den Aufrufer zurück. Beim nächsten Aufruf von next() setzt die Funktion exakt an dieser Stelle fort – mit demselben lokalen Scope, denselben Variablen, demselben Call-Stack-Zustand.
Das macht Generator Functions fundamental anders als normale Funktionen oder Arrays: Sie berechnen Werte nicht vorab und speichern sie, sondern produzieren jeden Wert erst, wenn er angefragt wird. Diese Eigenschaft nennt man Lazy Evaluation. Für einen Array mit einer Million Zahlen braucht JavaScript eine Million Speicherplätze. Eine Generator Function, die dieselbe Sequenz erzeugt, braucht konstanten Speicher – unabhängig davon, wie lang die Sequenz ist. Das ist besonders relevant für Datentransformations-Pipelines, Paginierungs-Logik, Dateiverarbeitung und jeden Fall, wo man nicht alle Daten auf einmal im Speicher halten möchte oder kann.
2. Das Iterator-Protokoll: next(), return() und throw()
Eine Generator Function gibt beim Aufruf kein Ergebnis zurück, sondern ein Generator-Objekt, das das Iterator-Protokoll implementiert. Dieses Protokoll definiert drei Methoden: next(value) setzt die Ausführung fort und gibt ein Objekt { value, done } zurück. return(value) beendet den Generator vorzeitig und führt alle finally-Blöcke aus. throw(error) wirft eine Exception an der aktuell pausierten yield-Stelle, als wäre sie dort entstanden – der Generator kann sie mit try/catch abfangen.
Das Generator-Objekt implementiert außerdem das Iterable-Protokoll: es hat eine [Symbol.iterator]()-Methode, die this zurückgibt. Das bedeutet, Generator-Objekte können direkt in for...of-Schleifen, Spread-Ausdrücken, Array.from() und destrukturierten Zuweisungen verwendet werden. Der for...of-Loop ruft intern next() auf und stoppt, wenn done: true zurückkommt. return() wird beim vorzeitigen Break aufgerufen, was sicherstellt, dass finally-Blöcke auch in diesem Fall ausgeführt werden – ein wichtiges Detail für Ressourcen-Cleanup in Generator Functions.
// Generator function basics — function* syntax and yield
function* counter(start = 0, step = 1) {
let current = start;
while (true) {
// Pauses here and returns current value to caller
const reset = yield current;
// next(true) signals a reset back to start
if (reset) {
current = start;
} else {
current += step;
}
}
}
const gen = counter(0, 5);
console.log(gen.next().value); // 0
console.log(gen.next().value); // 5
console.log(gen.next().value); // 10
console.log(gen.next(true).value); // 0 — reset triggered
console.log(gen.next().value); // 5
// Cleanup via return() — triggers finally blocks in the generator
gen.return('done'); // { value: 'done', done: true }
// Error injection via throw()
function* safeGen() {
try {
yield 1;
yield 2;
} catch (err) {
console.error('Caught inside generator:', err.message);
yield -1; // Recovery value after caught error
}
}
const sg = safeGen();
sg.next(); // { value: 1, done: false }
sg.throw(new Error('oops')); // Caught inside generator: oops → { value: -1, done: false }
3. Lazy Evaluation: Werte nur bei Bedarf berechnen
Der größte praktische Vorteil von Generator Functions ist Lazy Evaluation: Werte werden exakt dann berechnet, wenn der Konsument sie anfordert – nicht früher. Betrachtet man eine Transformations-Pipeline über einen großen Datensatz (filtern, mappen, auf die ersten zehn begrenzen), würde ein Array-basierter Ansatz alle Elemente filtern und mappen, bevor das Take greift. Eine Generator-Pipeline stoppt die Berechnung, sobald das Take-Limit erreicht ist – die übrigen Elemente werden nie berechnet.
Diese Eigenschaft hat reale Performance-Konsequenzen. Bei der Verarbeitung von CSV-Dateien mit Millionen von Zeilen lädt ein Array-basierter Ansatz die gesamte Datei in den Speicher. Ein Generator liest Zeile für Zeile, verarbeitet sie und gibt den Speicher sofort wieder frei. In Node.js verbindet sich das elegant mit dem Streams-API: ein Readable Stream kann als async iterable konsumiert werden, und Generator Functions können die eingehenden Chunks transformieren, ohne sie zu puffern. Das ist die Grundlage für speichereffiziente ETL-Pipelines in JavaScript.
4. Unendliche Sequenzen ohne Speicherprobleme
Eine Generator Function mit einer Endlosschleife ist kein Bug – sie ist ein nützliches Muster für unendliche Sequenzen. Fibonacci-Zahlen, Primzahlen, UUIDs, Zeitstempel, Paginierungs-Cursors: all das lässt sich als unendliche Sequenz modellieren, die der Konsument nach Bedarf konsumiert. Das Speicherproblem eines Arrays mit unendlichen Elementen besteht schlicht nicht, weil der Generator nur das aktuelle Element im Speicher hält.
Praktisches Beispiel: eine Paginierungs-Funktion, die automatisch die nächste API-Seite lädt, bis alle Ergebnisse konsumiert sind. Als Generator Function implementiert, braucht der Aufrufer nicht zu wissen, wie viele Seiten es gibt – er iteriert mit for await...of, und der Generator stoppt, wenn keine weiteren Seiten verfügbar sind. Das entkoppelt die Pagination-Logik vollständig von der Verarbeitungslogik. Der Konsument kann auch mittendrin mit break stoppen, was den Generator ordnungsgemäß beendet und alle Ressourcen freigibt.
// Infinite sequence — generates Fibonacci numbers on demand
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take only what you need — rest is never computed
function take(n, iterable) {
const result = [];
for (const item of iterable) {
result.push(item);
if (result.length >= n) break;
}
return result;
}
console.log(take(8, fibonacci())); // [0, 1, 1, 2, 3, 5, 8, 13]
// Infinite primes using Sieve of Eratosthenes (lazy)
function* primes() {
const composites = new Map();
let n = 2;
while (true) {
if (!composites.has(n)) {
yield n;
composites.set(n * n, [n]);
} else {
for (const p of composites.get(n)) {
const next = n + p;
if (composites.has(next)) composites.get(next).push(p);
else composites.set(next, [p]);
}
composites.delete(n);
}
n++;
}
}
console.log(take(10, primes())); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
5. yield* — Delegation an andere Iterables
yield* delegiert die Iteration an ein anderes Iterable – einen anderen Generator, ein Array, einen String oder jeden anderen iterierbaren Wert. Das ist das Mittel zur Komposition von Generator Functions: eine übergeordnete Generator Function kann die Werte mehrerer untergeordneter Generatoren nacheinander oder verschachtelt produzieren. Der Unterschied zu einer normalen Schleife mit yield: yield* leitet auch den Rückgabewert des delegierten Generators weiter, was bei bidirektionaler Kommunikation wichtig ist.
Ein praktischer Anwendungsfall für yield* ist das rekursive Traversieren von Baumstrukturen. Ein Baum-Generator kann sich selbst mit yield* für Kindknoten aufrufen und erzeugt so einen flachen Strom aller Knoten in Depth-First-Order – ohne expliziten Stack und ohne alle Knoten vorab in ein Array zu sammeln. Das ist eleganter und speicherschonender als jede Array-basierte Traversierung, weil zu jedem Zeitpunkt nur der aktuelle Pfad im Generator-Call-Stack gehalten wird.
6. Bidirektionale Kommunikation mit next(value)
Eine Generator Function kann nicht nur Werte ausgeben, sondern auch Werte vom Aufrufer empfangen. Der Trick: next(value) übergibt einen Wert, der als Ergebnis des aktuellen yield-Ausdrucks im Generator verfügbar ist. Das erste next() hat dabei keinen Wert-Parameter (er wird ignoriert, weil noch kein yield auf einen Wert wartet). Erst ab dem zweiten next(value) kommt der Wert im Generator an.
Dieses Pattern ermöglicht interaktive Generatoren, die auf äußere Steuerung reagieren: ein Parser, der Zeichen empfängt; ein Zustandsautomat, der Ereignisse verarbeitet; eine Coroutine, die auf Ergebnisse von Callbacks wartet. Bevor async/await eingeführt wurde, war genau das die Grundlage von co.js und anderen Generator-basierten Async-Bibliotheken: ein Runner rief next() auf, der Generator yieldte Promises, und der Runner wartete auf jedes Promise und injizierte das Ergebnis per next(result) zurück.
// Bidirectional communication — generator as a coroutine
function* accumulator() {
let total = 0;
let count = 0;
while (true) {
// Receives value from next(n), returns running average
const n = yield total === 0 ? null : total / count;
if (n === null) break; // null signals termination
total += n;
count++;
}
return total / count; // Final average returned via done: true
}
const avg = accumulator();
avg.next(); // Start generator — yields null (no data yet)
avg.next(10); // total=10, count=1 → yields 10
avg.next(20); // total=30, count=2 → yields 15
avg.next(30); // total=60, count=3 → yields 20
const final = avg.next(null); // done: true, value: 20
// yield* with return value — compose generators
function* inner() {
yield 'a';
yield 'b';
return 'inner-done'; // Return value passed to outer via yield*
}
function* outer() {
const result = yield* inner(); // Receives 'inner-done' as result
console.log('Inner completed with:', result);
yield 'c';
}
console.log([...outer()]); // ['a', 'b', 'c']
// Also logs: "Inner completed with: inner-done"
7. Async Generators: asynchrone Lazy Iteration
Async Generator Functions (async function*) kombinieren das Lazy-Evaluation-Pattern mit asynchronen Operationen. Statt synchroner Werte produzieren sie Promises, die der Konsument mit for await...of konsumiert. Das ist das native Pattern für asynchrone Streams in modernem JavaScript: API-Paginierung, Datenbankabfragen seitenweise, WebSocket-Nachrichten, Dateilesevorgänge in Node.js.
Der entscheidende Unterschied zu normalen Generator Functions: innerhalb einer async Generator Function kann man await verwenden, um auf Promises zu warten, bevor der nächste Wert geyieldet wird. Das erlaubt das Mischen von I/O-Operationen und Lazy-Evaluation ohne Callback-Pyramiden. Ein API-Paginierungs-Generator awaitet den Fetch-Request für jede Seite und yieldet dann jeden Eintrag der Seite einzeln – der Konsument sieht einen kontinuierlichen Strom von Einträgen, während der Generator im Hintergrund die nächste Seite lädt.
/**
* Async generator that paginates an API endpoint lazily.
* Loads next page only when consumer requests more items.
* @param {string} baseUrl - API base URL supporting ?page=N
* @param {AbortSignal} [signal]
* @yields {object} Individual items from each page
*/
async function* paginatedFetch(baseUrl, signal) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}&per_page=100`, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status} on page ${page}`);
const { data, meta } = await response.json();
// Yield each item individually — consumer gets a flat stream
for (const item of data) {
yield item;
}
hasMore = page < meta.total_pages;
page++;
}
}
// Consumer — stops early after 250 items without loading more pages
const controller = new AbortController();
let count = 0;
for await (const order of paginatedFetch('/api/orders', controller.signal)) {
await processOrder(order);
if (++count >= 250) {
controller.abort(); // Cancel in-flight request
break;
}
}
8. Generator-Pipelines für Datentransformation
Die eleganteste Anwendung von Generator Functions sind Transformations-Pipelines: eine Kette von Generatoren, wobei jeder Generator Werte vom vorherigen konsumiert, transformiert und an den nächsten weitergibt. Jeder Schritt ist lazy – eine Zeile aus der Quelle wandert durch die gesamte Pipeline und wird verarbeitet, bevor die nächste Zeile beginnt. Das ergibt constant-memory-Pipelines für beliebig große Datenmengen.
Generator-Pipelines sind komposierbar und testbar: jede Transformations-Generator-Function kann einzeln getestet werden. Die Komposition erfolgt durch einfaches Verschachteln der Aufrufe. Im Gegensatz zu Array-Methoden wie .filter().map().reduce(), die für jeden Schritt einen neuen vollständigen Array erzeugen, erzeugt eine Generator-Pipeline zu keinem Zeitpunkt einen Zwischenarray. Das ist besonders wichtig in Umgebungen mit begrenztem Heap wie Edge Functions oder Service Workern.
9. Generatoren vs. Arrays und Streams im Vergleich
Die Wahl zwischen Generator Functions, Arrays und Node.js Streams hängt vom Anwendungsfall ab. Arrays sind die richtige Wahl, wenn alle Daten gebraucht werden und die Menge überschaubar ist – einfach, durchsuchtbar, mehrfach iterierbar. Generatoren sind optimal für lazy, einmal-iterierbare Sequenzen beliebiger Länge. Node.js Streams unterstützen Backpressure und sind für hochperformante I/O ausgelegt, aber erheblich komplexer.
| Eigenschaft | Array | Generator Function | Node.js Stream |
|---|---|---|---|
| Speicherbedarf | O(n) – alle Elemente vorab | O(1) – nur aktueller Wert | O(highWaterMark) |
| Mehrfach iterierbar | Ja – beliebig oft | Nein – einmalig | Nein – einmalig |
| Unendliche Sequenzen | Nicht möglich | Ja – natürlich | Ja – mit Push-Streams |
| Async-Support | Nein (Promise.all nötig) | Ja – async function* | Ja – nativ async |
| Komplexität | Einfach | Mittel | Hoch (Backpressure, Events) |
In modernem JavaScript mit for await...of und dem Web Streams API verschmelzen die Grenzen zwischen Generator Functions und Streams. Node.js Readable Streams implementieren seit Version 16 das async iterable Protokoll, sodass man sie direkt in for await...of verwenden kann. Der Iterator Helpers Proposal (Stage 3) fügt native Methoden wie .map(), .filter(), .take() und .drop() direkt zum Iterator-Protokoll hinzu – die gleiche Ergonomie wie Arrays, aber lazy.
Mironsoft
JavaScript-Architektur, Datenpipelines und Performance-Optimierung
Speichereffiziente Datenpipelines für Ihre Anwendung?
Wir entwerfen Generator-basierte ETL-Pipelines und Datenverarbeitungs-Architektur, die auch bei Millionen von Datensätzen im konstanten Speicher bleibt – in Node.js, im Browser und an der Edge.
ETL-Pipelines
Generator-basierte Datentransformation ohne Speicher-Overhead für CSV, JSON und API-Daten
API-Paginierung
Async-Generator-Wrapper für paginierte APIs mit automatischer Fortsetzung und AbortSignal
Performance-Review
Identifikation von Array-Zwischenpuffern, die durch Lazy-Generator-Pipelines ersetzt werden können
10. Zusammenfassung
Generator Functions sind eines der mächtigsten und am meisten unterschätzten JavaScript-Features. function* und yield ermöglichen Lazy Evaluation, unendliche Sequenzen mit konstantem Speicherbedarf und bidirektionale Kommunikation zwischen Generator und Aufrufer. Das Iterator-Protokoll macht Generatoren nahtlos kompatibel mit for...of, Spread-Syntax und Array.from. yield* ermöglicht die Komposition von Generatoren und die elegante Traversierung rekursiver Strukturen. Async Generators verbinden Lazy Evaluation mit asynchronen I/O-Operationen für API-Paginierung, Dateiverarbeitung und Datenbankabfragen.
Die wichtigste Erkenntnis: Generator Functions sind nicht nur akademisch interessant, sondern lösen reale Probleme besser als Arrays. Immer wenn man einen Array aufbaut, um ihn anschließend zu filtern und zu mappen, ist eine Generator-Pipeline die speicher- und recheneffizientere Alternative. Async Generators sind das native Mittel für asynchrone Streams und ersetzen komplexe Event-Emitter-Patterns durch verständliches, synchron aussehendes Code.
Generator Functions — Das Wichtigste auf einen Blick
Lazy Evaluation
function* und yield erzeugen Werte on-demand. O(1) Speicherbedarf unabhängig von der Sequenzlänge – ideal für große Datenmengen.
Iterator-Protokoll
Generatoren sind direkt in for...of, Spread und Array.from verwendbar. return() für sauberen Cleanup, throw() für Fehlerinjektion.
Async Generators
async function* mit for await...of – das native Pattern für API-Paginierung, Dateiverarbeitung und Datenbankabfragen mit Lazy Loading.
Pipelines
Generator-Pipelines brauchen keine Zwischenarrays. yield* für Komposition. Jede Stufe einzeln testbar – besser als Array-Methodenketten für große Daten.
11. FAQ: JavaScript Generator Functions
1Generator vs. normale Funktion?
2Warum Generatoren statt Arrays?
3Break in for...of über Generator?
return() auf dem Generator auf – das triggert finally-Blöcke für sauberes Ressourcen-Cleanup.4Werte an Generator übergeben?
next(value) – der Wert wird als Ergebnis des aktuellen yield-Ausdrucks verfügbar. Erstes next() ignoriert den Parameter.5Was ist yield*?
6async function* vs. function*?
7Generator mehrmals iterieren?
8Fehler in Generator werfen?
gen.throw(error) wirft an der aktuellen yield-Stelle. Generator kann mit try/catch abfangen und mit Recovery-Wert weitermachen.