Skip to content

Türchen 12

Published: at 07:00 AMSuggest Changes

Generators – Iteration auf die nächste Stufe heben

Mit der Einführung von ES6 (ECMAScript 2015) hat JavaScript eine Reihe von mächtigen Features erhalten, darunter die Generatorfunktionen. Generators bieten eine neue Art, Funktionen zu schreiben, die die Ausführung pausieren und später fortsetzen können. Sie eröffnen Möglichkeiten für komplexe Iterationsmuster, asynchrone Programmierung und mehr. In diesem Artikel werden wir die Grundlagen von Generators erläutern und zeigen, wie sie deine Art zu programmieren revolutionieren können.

Was sind Generatorfunktionen?

Eine Generatorfunktion ist eine spezielle Art von Funktion, die während ihrer Ausführung mehrfach pausiert und wieder aufgenommen werden kann. Sie wird mit dem Schlüsselwort function* deklariert und verwendet das yield-Schlüsselwort, um Werte zu produzieren.

Merkmale von Generators:

Syntax einer Generatorfunktion

Deklaration einer Generatorfunktion:

function* generatorName() {
  // Generatorfunktion-Körper
}

Beispiel:

function* simpleGenerator() {
  console.log('Start');
  yield 1;
  console.log('Zwischenstand');
  yield 2;
  console.log('Ende');
}

Verwendung von Generatorfunktionen

Um einen Generator zu nutzen, rufst du die Generatorfunktion auf, wodurch ein Generatorobjekt zurückgegeben wird. Dieses Objekt besitzt eine next()-Methode, mit der du durch die yield-Anweisungen navigieren kannst.

Beispiel:

const gen = simpleGenerator();

gen.next();
// Ausgabe:
// Start
// { value: 1, done: false }

gen.next();
// Ausgabe:
// Zwischenstand
// { value: 2, done: false }

gen.next();
// Ausgabe:
// Ende
// { value: undefined, done: true }

Erläuterung:

Praktische Anwendungsfälle

1. Erstellen von iterierbaren Objekten

Generatoren können verwendet werden, um benutzerdefinierte iterierbare Objekte zu erstellen.

Beispiel: Fibonacci-Folge

function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();

console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3

2. Steuerung des Iterationsverhaltens

Mit Generators kannst du komplexe Iterationsmuster implementieren, z. B. über verschachtelte Datenstrukturen iterieren.

Beispiel: Iteration über einen Baum

function* treeTraversal(node) {
  if (node) {
    yield node.value;
    yield* treeTraversal(node.left);
    yield* treeTraversal(node.right);
  }
}

// Beispielbaum
const tree = {
  value: 1,
  left: {
    value: 2,
    left: null,
    right: { value: 3, left: null, right: null }
  },
  right: {
    value: 4,
    left: null,
    right: null
  }
};

for (let value of treeTraversal(tree)) {
  console.log(value);
}
// Ausgabe:
// 1
// 2
// 3
// 4

3. Asynchrone Programmierung

Vor der Einführung von async/await wurden Generatoren verwendet, um asynchrone Abläufe zu steuern.

Beispiel mit async/await:

async function* getHomeworld(id) {
    const person = await fetch(`https://swapi.dev/api/people/${id}/`).then((result) => result.json());
    const homeworld = await fetch(person.homeworld).then((result) => result.json());

    yield { person, homeworld };
}

// usage of async generator function
(async () => {
    for (let id = 1; id <= 5; id++) {
        const homeworld = getHomeworld(id);
        const { value: data } = await homeworld.next();
    
        console.log(`${data.person.name} (${data.homeworld.name})`);
    }
})();

Bidirektionale Kommunikation

Du kannst nicht nur Werte von der Generatorfunktion erhalten, sondern auch Werte an sie zurückgeben.

Beispiel:

function* interactiveGenerator() {
  const name = yield 'Wie heißt du?';
  const age = yield `Hallo, ${name}! Wie alt bist du?`;
  return `Du bist ${age} Jahre alt.`;
}

const gen = interactiveGenerator();

console.log(gen.next().value);           // 'Wie heißt du?'
console.log(gen.next('Anna').value);     // 'Hallo, Anna! Wie alt bist du?'
console.log(gen.next(30).value);         // 'Du bist 30 Jahre alt.'

Generatoren als Iteratoren

Generatoren implementieren das Iterator-Protokoll und können daher direkt in Schleifen verwendet werden.

Beispiel:

function* countUpTo(max) {
  let count = 1;
  while (count <= max) {
    yield count++;
  }
}

for (let num of countUpTo(5)) {
  console.log(num);
}
// Ausgabe:
// 1
// 2
// 3
// 4
// 5

Fehlerbehandlung in Generatoren

Du kannst Fehler an Generatoren senden und innerhalb der Generatorfunktion behandeln.

Beispiel:

function* errorHandlingGenerator() {
  try {
    yield 'Alles gut bis hierhin.';
    yield 'Noch immer gut.';
  } catch (error) {
    console.log('Fehler innerhalb des Generators:', error.message);
  }
}

const gen = errorHandlingGenerator();

console.log(gen.next().value); // 'Alles gut bis hierhin.'
console.log(gen.next().value); // 'Noch immer gut.'
gen.throw(new Error('Etwas ist schiefgelaufen')); // 'Fehler innerhalb des Generators: Etwas ist schiefgelaufen'

Vergleich mit normalen Funktionen

Praktische Anwendungen

Zum Abschluss hier noch ein paar praktische Fälle, in denen Generatorfunctions genutzt werden können.

1. Datenströme verarbeiten

Angenommen, du hast einen großen Datensatz, der nicht komplett ins Gedächtnis geladen werden soll, sondern Stück für Stück verarbeitet wird. Mit Generatoren kannst du diese Daten in handliche Portionen zerlegen:

function* chunkGenerator(data, chunkSize) {
  let index = 0;
  while (index < data.length) {
    yield data.slice(index, index + chunkSize);
    index += chunkSize;
  }
}

// Beispiel für große Datenmenge
const largeData = Array.from({ length: 100000 }, (_, i) => i);

// Wir holen die Daten in Blöcken von 1000 Elementen
for (const chunk of chunkGenerator(largeData, 1000)) {
  console.log('Verarbeite Chunk mit Länge:', chunk.length);
  // Hier kannst du jeden Chunk einzeln verarbeiten, ohne alles auf einmal laden zu müssen.
}

Hier werden Generatoren genutzt, um große Datenmengen effizient in Batches (Chunks) zu verarbeiten, wodurch Speicher gespart wird.

In diesem Codebeispiel wird eine Generatorfunktion chunkGenerator definiert, die große Datenmengen in kleinere Blöcke (Chunks) aufteilt. Die Funktion nimmt zwei Argumente entgegen: data, das die gesamte Datenmenge repräsentiert, und chunkSize, das die Größe jedes Chunks angibt.

Die Generatorfunktion verwendet eine while-Schleife, um durch die Daten zu iterieren. In jeder Iteration wird ein Teil der Daten mit der Methode slice extrahiert, beginnend beim aktuellen index und endend bei index + chunkSize. Dieser Teil der Daten wird mit yield zurückgegeben, wodurch die Ausführung der Funktion pausiert und der Chunk an den Aufrufer zurückgegeben wird. Der index wird dann um chunkSize erhöht, um zum nächsten Chunk zu gelangen.

Dieses Beispiel zeigt, wie Generatorfunktionen verwendet werden können, um große Datenmengen effizient in kleinere, handhabbare Teile zu zerlegen und zu verarbeiten.

2. Implementierung von lazy Evaluation

Statt alle Werte im Voraus zu berechnen, liefert der Generator immer nur dann einen Wert, wenn er auch wirklich benötigt wird. Dies spart Rechenleistung und Ressourcen:

function* lazyFibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fibGen = lazyFibonacci();

// Es werden nur so viele Fibonacci-Zahlen erzeugt, wie tatsächlich abgerufen werden:
console.log(fibGen.next().value); // 0
console.log(fibGen.next().value); // 1
console.log(fibGen.next().value); // 1
console.log(fibGen.next().value); // 2
// ... und so weiter

Statt ein komplettes Array von Fibonacci-Zahlen anzulegen, generiert der Generator nur dann den nächsten Wert, wenn next() aufgerufen wird.

In diesem Codebeispiel wird eine Generatorfunktion lazyFibonacci definiert, die eine unendliche Folge von Fibonacci-Zahlen erzeugt. Die Fibonacci-Folge ist eine Zahlenreihe, bei der jede Zahl die Summe der beiden vorhergehenden Zahlen ist, beginnend mit 0 und 1.

Die Generatorfunktion verwendet zwei Variablen a und b, die initial auf 0 bzw. 1 gesetzt sind. In einer while (true)-Schleife wird die aktuelle Zahl a mit yield zurückgegeben, wodurch die Ausführung der Funktion pausiert und die Zahl an den Aufrufer zurückgegeben wird. Anschließend werden die Werte von a und b aktualisiert, sodass a den Wert von b annimmt und b die Summe der vorherigen Werte von a und b wird.

Die Konsolenausgaben zeigen, dass nur so viele Fibonacci-Zahlen erzeugt werden, wie tatsächlich abgerufen werden:

Der erste Aufruf von fibGen.next().value gibt 0 zurück. Der zweite Aufruf gibt 1 zurück. Der dritte Aufruf gibt ebenfalls 1 zurück. Der vierte Aufruf gibt 2 zurück. Dieses Beispiel zeigt, wie Generatorfunktionen verwendet werden können, um unendliche Folgen zu erzeugen, wobei die Werte nur bei Bedarf berechnet werden. Dies ist besonders nützlich, um Speicherplatz zu sparen und die Berechnung auf das Nötigste zu beschränken.

3. State Machines

Generatoren können verwendet werden, um Zustandsmaschinen abzubilden. Je nach Input kann der Generator entscheiden, in welchen Zustand er wechselt, und so auf unterschiedliche Eingaben reagieren:

function* trafficLight() {
  while (true) {
    console.log('Rot - Bitte warten');
    yield 'RED';

    console.log('Grün - Du darfst gehen');
    yield 'GREEN';

    console.log('Gelb - Achtung, bald wieder Rot');
    yield 'YELLOW';
  }
}

const light = trafficLight();

// Jeder next()-Aufruf repräsentiert den Übergang zum nächsten Zustand:
console.log(light.next().value); // 'RED'
console.log(light.next().value); // 'GREEN'
console.log(light.next().value); // 'YELLOW'
console.log(light.next().value); // 'RED' (zurück zum Anfang)

// Wir könnten diesen Generator nun in eine logikgesteuerte Schleife integrieren,
// die je nach externer Eingabe (etwa ein Timer oder Sensor) einen weiteren .next() Aufruf tätigt.

Dieses einfache Beispiel einer Ampel-Steuerung zeigt, wie ein Generator nacheinander verschiedene Zustände (Rot, Grün, Gelb) durchläuft. Diese Idee lässt sich auf komplexere Zustandsmaschinen übertragen, in denen Input-Werte (z. B. aus next()-Argumenten) den Übergang in verschiedene Zweige der Ausführung bestimmen.

In diesem Codebeispiel wird eine Generatorfunktion trafficLight definiert, die die Zustände einer Ampel simuliert. Die Funktion verwendet eine while (true)-Schleife, um unendlich zwischen den Zuständen “Rot”, “Grün” und “Gelb” zu wechseln.

Innerhalb der Schleife wird zuerst der Zustand “Rot” mit einer Konsolennachricht und yield ‘RED’ ausgegeben, wodurch die Ausführung der Funktion pausiert und der Zustand an den Aufrufer zurückgegeben wird. Danach wird der Zustand “Grün” mit yield ‘GREEN’ und schließlich der Zustand “Gelb” mit yield ‘YELLOW’ ausgegeben. Nach dem Gelb-Zustand beginnt die Schleife wieder von vorne.

Ein Generator-Objekt light wird durch Aufruf der trafficLight-Funktion erstellt. Dieses Objekt kann verwendet werden, um die Zustände der Ampel nacheinander abzurufen. Mit jedem Aufruf der Methode next wird der nächste Zustand in der Folge erzeugt und zurückgegeben.

Die Konsolenausgaben zeigen die Zustandsübergänge der Ampel:

Der erste Aufruf von light.next().value gibt ‘RED’ zurück und zeigt die Nachricht “Rot - Bitte warten”. Der zweite Aufruf gibt ‘GREEN’ zurück und zeigt die Nachricht “Grün - Du darfst gehen”. Der dritte Aufruf gibt ‘YELLOW’ zurück und zeigt die Nachricht “Gelb - Achtung, bald wieder Rot”. Der vierte Aufruf gibt wieder ‘RED’ zurück, da die Schleife von vorne beginnt. Dieses Beispiel zeigt, wie Generatorfunktionen verwendet werden können, um zyklische Zustände zu simulieren, wobei die Zustände nur bei Bedarf berechnet und zurückgegeben werden.

Fazit

Generatoren sind ein mächtiges Feature in JavaScript, das neue Möglichkeiten für die Kontrolle des Ausführungsflusses bietet. Sie ermöglichen es, komplexe Iterationsmuster zu implementieren, die asynchrone Programmierung zu vereinfachen und die bidirektionale Kommunikation innerhalb von Funktionen zu nutzen. Obwohl sie nicht immer die erste Wahl für jeden Anwendungsfall sind, können sie in den richtigen Situationen den Code erheblich vereinfachen und effizienter gestalten.

Erkunde die Welt der Generatoren und entdecke, wie sie deine JavaScript-Programmierung auf die nächste Stufe heben können!


Previous Post
Türchen 13
Next Post
Türchen 11