Generatoren und Iteratoren sind leistungsstarke Konzepte in JavaScript, die mit ECMAScript 2015 (ES6) eingeführt wurden. Sie bieten elegante Lösungen für die sequentielle Verarbeitung von Daten und ermöglichen neue Muster für asynchrone Programmierung. In diesem Abschnitt werden wir beide Konzepte im Detail untersuchen und ihre praktischen Anwendungen kennenlernen.
Ein Iterator ist ein Objekt, das das Iteration Protocol in JavaScript implementiert und eine standardisierte Möglichkeit bietet, sequentiell auf Elemente einer Sammlung zuzugreifen.
Das Iteration Protocol besteht aus zwei Teilen:
Iterable Protocol: Definiert, wie ein
JavaScript-Objekt seine Iteration verhalten festlegt. Ein Objekt ist ein
Iterable, wenn es eine Methode mit dem Schlüssel
Symbol.iterator implementiert, die einen Iterator
zurückgibt.
Iterator Protocol: Definiert eine
standardisierte Methode, um sequentielle Werte zu produzieren. Ein
Objekt ist ein Iterator, wenn es eine next()-Methode
besitzt, die ein Objekt mit den Eigenschaften value und
done zurückgibt.
// Ein einfacher Iterator
function createIterator(array) {
let index = 0;
return {
next: function() {
if (index < array.length) {
return { value: array[index++], done: false };
} else {
return { done: true };
}
}
};
}
const iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { done: true }JavaScript bietet viele eingebaute Datenstrukturen, die bereits das Iterable-Protocol implementieren:
arguments-Objekt// Array als Iterable
const array = ['a', 'b', 'c'];
for (const element of array) {
console.log(element); // 'a', 'b', 'c'
}
// String als Iterable
const string = "hello";
for (const char of string) {
console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
// Map als Iterable
const map = new Map([
['name', 'Max'],
['age', 30]
]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`); // 'name: Max', 'age: 30'
}
// Set als Iterable
const set = new Set([1, 2, 3, 2, 1]);
for (const num of set) {
console.log(num); // 1, 2, 3
}Wir können auch eigene Iterables erstellen, indem wir die
Symbol.iterator-Methode implementieren:
// Ein benutzerdefiniertes Iterable
const customIterable = {
data: [10, 20, 30],
// Symbol.iterator-Methode, die einen Iterator zurückgibt
[Symbol.iterator]() {
let index = 0;
return {
// Referenz auf das äußere Objekt
context: this,
// next-Methode des Iterators
next() {
if (index < this.context.data.length) {
return { value: this.context.data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
// Verwendung mit for...of
for (const item of customIterable) {
console.log(item); // 10, 20, 30
}
// Spread-Operator mit Iterables
const numbers = [...customIterable]; // [10, 20, 30]
// Destrukturierung mit Iterables
const [first, second, third] = customIterable; // first=10, second=20, third=30Generatoren sind spezielle Funktionen, die ihre Ausführung unterbrechen und später an derselben Stelle fortsetzen können. Sie bieten eine elegante Möglichkeit, Iteratoren zu erstellen.
Generator-Funktionen werden mit einem Sternchen (*)
definiert und verwenden das yield-Schlüsselwort, um Werte
zurückzugeben und die Ausführung zu pausieren:
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = simpleGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }Generatoren können nicht nur Werte zurückgeben, sondern auch Werte beim Fortsetzen empfangen:
function* communicatingGenerator() {
const x = yield 'Bitte gib einen Wert ein:';
const y = yield `Dein Wert (${x}) wurde verarbeitet. Bitte gib einen weiteren Wert ein:`;
yield `Deine Werte addiert ergeben: ${x + y}`;
}
const generator = communicatingGenerator();
console.log(generator.next().value);
// 'Bitte gib einen Wert ein:'
console.log(generator.next(10).value);
// 'Dein Wert (10) wurde verarbeitet. Bitte gib einen weiteren Wert ein:'
console.log(generator.next(20).value);
// 'Deine Werte addiert ergeben: 30'Jeder Generator implementiert automatisch sowohl das Iterable- als auch das Iterator-Protocol:
function* countUpTo(max) {
let count = 1;
while (count <= max) {
yield count++;
}
}
// Verwendung mit for...of
for (const num of countUpTo(5)) {
console.log(num); // 1, 2, 3, 4, 5
}
// Spread-Operator
const numbers = [...countUpTo(5)]; // [1, 2, 3, 4, 5]
// Destrukturierung
const [first, second, ...rest] = countUpTo(5); // first=1, second=2, rest=[3, 4, 5]Mit dem yield*-Ausdruck können wir die Kontrolle an
einen anderen Generator oder ein Iterable delegieren:
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield 'a';
yield 'b';
}
function* combined() {
yield* gen1();
yield* gen2();
yield 'Ende';
}
for (const value of combined()) {
console.log(value); // 1, 2, 'a', 'b', 'Ende'
}Generatoren eignen sich hervorragend für die Erstellung potenziell unendlicher Sequenzen:
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value); // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
}Generatoren vereinfachen das Traversieren komplexer Datenstrukturen wie Bäume:
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
function* inOrderTraversal(node) {
if (node) {
yield* inOrderTraversal(node.left);
yield node.value;
yield* inOrderTraversal(node.right);
}
}
// Erstellen eines Beispielbaums
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
// Durchlaufen des Baums
for (const value of inOrderTraversal(root)) {
console.log(value); // 4, 2, 5, 1, 3
}Generatoren eignen sich gut für Paging-Szenarien, bei denen Daten schrittweise geladen werden:
async function* fetchPaginatedData(url, pageSize = 10) {
let page = 1;
let hasMoreData = true;
while (hasMoreData) {
const response = await fetch(`${url}?page=${page}&pageSize=${pageSize}`);
const data = await response.json();
if (data.items.length === 0) {
hasMoreData = false;
} else {
yield* data.items;
page++;
}
}
}
// Verwendung
async function loadData() {
const dataGenerator = fetchPaginatedData('/api/items', 20);
// Laden der ersten 100 Elemente
let count = 0;
for await (const item of dataGenerator) {
processItem(item);
count++;
if (count >= 100) break;
}
}Mit ECMAScript 2018 wurden asynchrone Generatoren und Iteratoren eingeführt, die besonders für asynchrone Datenströme nützlich sind.
Ein asynchroner Iterator ist ähnlich wie ein regulärer Iterator, aber
seine next()-Methode gibt ein Promise zurück, das zu einem
Objekt mit value und done aufgelöst wird:
const asyncIterable = {
async *[Symbol.asyncIterator]() {
for (let i = 1; i <= 5; i++) {
// Simuliere asynchrone Arbeit
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
};
(async () => {
for await (const num of asyncIterable) {
console.log(num); // Ausgabe 1-5 im Abstand von je 1 Sekunde
}
})();Asynchrone Generator-Funktionen werden mit
async function* definiert und können sowohl
await als auch yield verwenden:
async function* fetchCommentsForPosts(posts) {
for (const post of posts) {
const response = await fetch(`/api/posts/${post.id}/comments`);
const comments = await response.json();
yield { post, comments };
}
}
async function displayPostComments(posts) {
const commentGenerator = fetchCommentsForPosts(posts);
for await (const { post, comments } of commentGenerator) {
console.log(`Post: ${post.title}`);
console.log(`Kommentare: ${comments.length}`);
}
}Generatoren eignen sich hervorragend für die Implementierung von Zustandsmaschinen:
function* trafficLightStateMachine() {
while (true) {
yield 'rot';
yield 'grün';
yield 'gelb';
}
}
const trafficLight = trafficLightStateMachine();
console.log(trafficLight.next().value); // 'rot'
console.log(trafficLight.next().value); // 'grün'
console.log(trafficLight.next().value); // 'gelb'
console.log(trafficLight.next().value); // 'rot'Generatoren können als Coroutinen verwendet werden, um komplexe Kontrollflüsse zu implementieren:
function* processData() {
const data = yield 'Daten laden';
const filteredData = yield `Verarbeite ${data.length} Datensätze`;
yield `Fertig: ${filteredData} Ergebnisse gefunden`;
}
function runCoroutine(generator, initialValue) {
const iterator = generator();
function handle(result, value) {
if (result.done) return result.value;
console.log(result.value);
return handle(iterator.next(value), performTask(result.value));
}
return handle(iterator.next(), initialValue);
}
function performTask(instruction) {
// Simuliere verschiedene Aufgaben basierend auf der Anweisung
if (instruction === 'Daten laden') {
return Array(100).fill().map((_, i) => `Item ${i}`);
}
if (instruction.includes('Verarbeite')) {
return 42; // Anzahl der gefilterten Ergebnisse
}
return null;
}
runCoroutine(processData);
// Ausgabe:
// 'Daten laden'
// 'Verarbeite 100 Datensätze'
// 'Fertig: 42 Ergebnisse gefunden'Vor der Einführung von async/await wurden Generatoren
oft verwendet, um asynchronen Code lesbarer zu gestalten:
function runAsync(generatorFunction) {
const generator = generatorFunction();
function handle(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value)
.then(value => handle(generator.next(value)))
.catch(error => handle(generator.throw(error)));
}
return handle(generator.next());
}
// Verwendung
runAsync(function* () {
try {
const users = yield fetch('/api/users').then(r => r.json());
const firstUser = users[0];
const posts = yield fetch(`/api/users/${firstUser.id}/posts`).then(r => r.json());
console.log(posts);
} catch (error) {
console.error('Fehler:', error);
}
});
// Dies ist ähnlich zu folgendem async/await Code:
async function fetchData() {
try {
const users = await fetch('/api/users').then(r => r.json());
const firstUser = users[0];
const posts = await fetch(`/api/users/${firstUser.id}/posts`).then(r => r.json());
console.log(posts);
} catch (error) {
console.error('Fehler:', error);
}
}Einer der größten Vorteile von Generatoren ist die Lazy Evaluation - Werte werden nur bei Bedarf berechnet:
function* rangeGenerator(start, end) {
for (let i = start; i <= end; i++) {
console.log(`Generiere Zahl ${i}`);
yield i;
}
}
const range = rangeGenerator(1, 1000000); // Kein Code wird ausgeführt
// Erst bei Anforderung wird der erste Wert berechnet
console.log(range.next().value); // Logs: "Generiere Zahl 1" und "1"
console.log(range.next().value); // Logs: "Generiere Zahl 2" und "2"
// Der Rest der Sequenz wird niemals berechnet, wenn nicht angefordertDurch Lazy Evaluation können Generatoren mit potenziell riesigen Datenmengen arbeiten, ohne viel Speicher zu verbrauchen:
// Verarbeitung einer großen Datei Zeile für Zeile mit Generatoren
async function* readFileByLine(filePath) {
const fileHandle = await fs.promises.open(filePath, 'r');
const reader = fileHandle.createReadStream();
let buffer = '';
try {
for await (const chunk of reader) {
buffer += chunk.toString();
let lineEnd;
while ((lineEnd = buffer.indexOf('\n')) !== -1) {
yield buffer.slice(0, lineEnd);
buffer = buffer.slice(lineEnd + 1);
}
}
if (buffer.length > 0) {
yield buffer;
}
} finally {
await fileHandle.close();
}
}
// Verwendung
async function processHugeFile(filePath) {
for await (const line of readFileByLine(filePath)) {
processLine(line);
}
}Wir können eigene Array-ähnliche Methoden mit Generatoren implementieren:
function* map(iterable, mapFn) {
for (const item of iterable) {
yield mapFn(item);
}
}
function* filter(iterable, filterFn) {
for (const item of iterable) {
if (filterFn(item)) {
yield item;
}
}
}
function* take(iterable, limit) {
let count = 0;
for (const item of iterable) {
if (count >= limit) break;
yield item;
count++;
}
}
// Funktionale Pipeline mit Generatoren
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = [...take(
filter(
map(numbers, x => x * 2),
x => x > 5
),
3
)];
console.log(result); // [6, 8, 10]Generatoren können genutzt werden, um Datastreams zu implementieren:
class DataStream {
constructor(generator) {
this.generator = generator;
}
map(fn) {
const self = this;
return new DataStream(function* () {
for (const item of self.generator()) {
yield fn(item);
}
});
}
filter(fn) {
const self = this;
return new DataStream(function* () {
for (const item of self.generator()) {
if (fn(item)) {
yield item;
}
}
});
}
take(limit) {
const self = this;
return new DataStream(function* () {
let count = 0;
for (const item of self.generator()) {
if (count >= limit) break;
yield item;
count++;
}
});
}
// Materialisiert den Stream zu einem Array
toArray() {
return [...this.generator()];
}
}
// Verwendung
const stream = new DataStream(function* () {
for (let i = 1; i <= 100; i++) {
yield i;
}
});
const result = stream
.filter(x => x % 2 === 0) // Nur gerade Zahlen
.map(x => x * x) // Quadrieren
.take(5) // Nur die ersten 5
.toArray(); // Materialisieren
console.log(result); // [4, 16, 36, 64, 100]