Apr 23, 2018 – 10 min read

ES6 führte eine neue Art der Arbeit mit Funktionen und Iteratoren in Form von Generatoren (oder Generatorfunktionen) ein. Ein Generator ist eine Funktion, die auf halbem Weg anhalten und dann an der Stelle fortfahren kann, an der sie aufgehört hat. Kurz gesagt, ein Generator scheint eine Funktion zu sein, aber er verhält sich wie ein Iterator.

Fun Fact: async/await kann auf Generatoren basieren. Lesen Sie hier mehr.

Generatoren sind eng mit Iteratoren verknüpft. Wenn Sie Iteratoren nicht kennen, finden Sie hier einen Artikel, der Ihnen hilft, sie besser zu verstehen.

Hier ist eine einfache Analogie, um ein Gefühl für Generatoren zu bekommen, bevor wir mit den technischen Details fortfahren.

Stellen Sie sich vor, Sie lesen einen spannenden Techno-Thriller. Ganz in die Seiten des Buches vertieft, hören Sie kaum, dass es an Ihrer Tür klingelt. Es ist der Pizzabote. Sie stehen auf, um die Tür zu öffnen. Doch vorher setzen Sie ein Lesezeichen bei der letzten Seite, die Sie gelesen haben. Du speicherst im Geiste die Ereignisse der Handlung. Dann gehst du los und holst deine Pizza. Sobald du in dein Zimmer zurückkehrst, beginnst du das Buch auf der Seite, auf der du das Lesezeichen gesetzt hast. Du fängst nicht wieder auf der ersten Seite an. In gewissem Sinne hast du als Generatorfunktion gehandelt.

Lassen Sie uns sehen, wie wir Generatoren nutzen können, um einige häufige Probleme beim Programmieren zu lösen. Aber vorher wollen wir definieren, was Generatoren sind.

Was sind Generatoren?

Eine normale Funktion wie diese kann nicht gestoppt werden, bevor sie ihre Aufgabe beendet hat, d.h. ihre letzte Zeile ausgeführt ist. Sie folgt dem so genannten „run-to-completion“-Modell.

function normalFunc() {
console.log('I')
console.log('cannot')
console.log('be')
console.log('stopped.')
}

Die einzige Möglichkeit, die normalFunc Funktion zu beenden, besteht darin, sie returnzu verlassen oder throweinen Fehler zu melden. Wenn man die Funktion erneut aufruft, beginnt sie die Ausführung wieder von vorne.

Im Gegensatz dazu ist ein Generator eine Funktion, die auf halbem Weg anhalten und dann an der Stelle fortfahren kann, an der sie aufgehört hat.

Hier sind einige andere allgemeine Definitionen von Generatoren –

  • Generatoren sind eine spezielle Klasse von Funktionen, die die Aufgabe des Schreibens von Iteratoren vereinfachen.
  • Ein Generator ist eine Funktion, die anstelle eines einzelnen Wertes eine Folge von Ergebnissen erzeugt, d.h. Sie erzeugen eine Reihe von Werten.

In JavaScript ist ein Generator eine Funktion, die ein Objekt zurückgibt, auf dem Sie next() aufrufen können. Jeder Aufruf von next() liefert ein Objekt der Form –

{ 
value: Any,
done: true|false
}

Die Eigenschaft value enthält den Wert. Die Eigenschaft done ist entweder true oder false. Wenn done zu true wird, hält der Generator an und erzeugt keine weiteren Werte mehr.

Hier ist eine Illustration desselben –

Normale Funktionen vs. Generatoren

Beachten Sie den gestrichelten Pfeil, der die Rendite-Resume-Rendite-Schleife kurz vor dem Ende im Generatoren-Teil des Bildes schließt. Es besteht die Möglichkeit, dass ein Generator nie beendet wird. Wir werden später ein Beispiel sehen.

Erstellen eines Generators

Lassen Sie uns sehen, wie wir einen Generator in JavaScript erstellen können –

function * generatorFunction() { // Line 1
console.log('This will be executed first.');
yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
yield 'World!';
}const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined

Konzentrieren Sie sich auf die fettgedruckten Teile. Um eine Generatorfunktion zu erstellen, verwenden wir die Syntax function * statt nur function. Zwischen dem Schlüsselwort function, * und dem Funktionsnamen können beliebig viele Leerzeichen stehen. Da es sich nur um eine Funktion handelt, können Sie sie überall dort verwenden, wo eine Funktion verwendet werden kann, d.h. innerhalb von Objekten und Klassenmethoden.

Innerhalb des Funktionskörpers haben wir kein return. Stattdessen haben wir ein weiteres Schlüsselwort yield (Zeile 2). Es ist ein Operator, mit dem ein Generator sich selbst anhalten kann. Jedes Mal, wenn ein Generator auf ein yield stößt, „gibt“ er den Wert zurück, der dahinter angegeben ist. In diesem Fall wird Hello, zurückgegeben. Allerdings sagen wir im Zusammenhang mit Generatoren nicht „zurückgegeben“. Wir sagen: „Der Generator hat Hello, geliefert“.

Wir können auch von einem Generator zurückkehren. Allerdings setzt return die done-Eigenschaft auf true, wonach der Generator keine weiteren Werte mehr erzeugen kann.

function * generatorFunc() {
yield 'a';
return 'b'; // Generator ends here.
yield 'a'; // Will never be executed.
}

In Zeile 3 erstellen wir das Generatorobjekt. Es scheint, als ob wir die Funktion generatorFunction aufrufen. Das tun wir in der Tat! Der Unterschied besteht darin, dass eine Generatorfunktion nicht irgendeinen Wert, sondern immer ein Generatorobjekt zurückgibt. Das Generator-Objekt ist ein Iterator. Sie können es also in for-of-Schleifen oder anderen Funktionen verwenden, die eine Iterable akzeptieren.

In Zeile 4 rufen wir die next()-Methode für generatorObject auf. Mit diesem Aufruf beginnt der Generator mit der Ausführung. Zuerst console.log das This will be executed first. Dann trifft er auf ein yield 'Hello, '. Der Generator gibt den Wert als Objekt { value: 'Hello, ', done: false } aus und pausiert. Jetzt wartet er auf den nächsten Aufruf.

In Zeile 5 rufen wir erneut next() auf. Diesmal wacht der Generator auf und beginnt mit der Ausführung, wo er aufgehört hat. Die nächste Zeile, die er findet, ist eine console.log. Er protokolliert die Zeichenfolge I will be printed after the pause. Eine weitere yield wird gefunden. Der Wert wird als das Objekt { value: 'World!', done: false } ausgegeben. Wir extrahieren die Eigenschaft value und protokollieren sie. Der Generator schläft wieder.

In Zeile 6 rufen wir erneut next() auf. Diesmal gibt es keine weiteren Zeilen, die ausgeführt werden müssen. Denken Sie daran, dass jede Funktion implizit undefined zurückgibt, wenn keine Rückgabeanweisung angegeben wird. Der Generator gibt also ein Objekt { value: undefined, done: true} zurück (anstatt es zu liefern). Das done wird auf true gesetzt. Dies signalisiert das Ende dieses Generators. Jetzt kann er keine weiteren Werte mehr erzeugen oder fortfahren, da keine weiteren Anweisungen mehr ausgeführt werden müssen.

Wir müssen ein neues anderes Generatorobjekt erzeugen, um den Generator erneut auszuführen.

Verwendungen von Generatoren

Es gibt viele großartige Anwendungsfälle für Generatoren. Sehen wir uns ein paar davon an.

Iterables implementieren

Wenn man einen Iterator implementiert, muss man manuell ein Iterator-Objekt mit einer next()-Methode erstellen. Außerdem müssen Sie den Zustand manuell speichern. Oftmals ist das sehr schwierig. Da Generatoren auch Iterables sind, können sie verwendet werden, um Iterables ohne den zusätzlichen Boilerplate-Code zu implementieren. Sehen wir uns ein einfaches Beispiel an.

Problem: Wir möchten eine benutzerdefinierte Iterable erstellen, die This, is und iterable. zurückgibt. Hier ist eine Implementierung mit Iteratoren –

const iterableObj = {
() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: 'This', done: false};
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable.', done: false};
}
return { value: '', done: true };
}
}
},
}for (const val of iterableObj) {
console.log(val);
}// This
// is
// iterable.

Hier ist das gleiche mit Generatoren –

function * iterableObj() {
yield 'This';
yield 'is';
yield 'iterable.'
}for (const val of iterableObj()) {
console.log(val);
}// This
// is
// iterable.

Sie können beide Versionen vergleichen. Es stimmt, dass dies ein etwas konstruiertes Beispiel ist. Aber es veranschaulicht die Punkte –

  • Wir müssen uns nicht um Symbol.iterator
  • Wir müssen nicht next().
  • Wir müssen das Rückgabeobjekt von next() d.h. { value: 'This', done: false }.
  • Wir müssen den Zustand nicht speichern. Im Beispiel des Iterators wurde der Zustand in der Variablen step gespeichert. Ihr Wert definierte, was von der Iterable ausgegeben wurde. Im Generator mussten wir nichts dergleichen tun.

Bessere Async-Funktionalität

Code, der Versprechen und Rückrufe wie –

function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}

verwendet, kann (mit Hilfe von Bibliotheken wie co.js)-

const fetchJson = co.wrap(function * (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});

Einigen Lesern ist vielleicht aufgefallen, dass es Parallelen zur Verwendung von async/await gibt. Das ist kein Zufall. async/await kann eine ähnliche Strategie verfolgen und ersetzt die Ausbeute durch await in Fällen, in denen Versprechen beteiligt sind. Es kann auf Generatoren beruhen. Siehe diesen Kommentar für weitere Informationen.

Unendliche Datenströme

Es ist möglich, Generatoren zu erstellen, die niemals enden. Betrachten wir dieses Beispiel –

function * naturalNumbers() {
let num = 1;
while (true) {
yield num;
num = num + 1
}
}const numbers = naturalNumbers();console.log(numbers.next().value)
console.log(numbers.next().value)// 1
// 2

Wir erstellen einen Generator naturalNumbers. Innerhalb der Funktion haben wir eine unendliche while Schleife. In dieser Schleife machen wir yield den num. Wenn der Generator nachgibt, wird er ausgesetzt. Wenn wir next() erneut aufrufen, wacht der Generator auf, macht dort weiter, wo er unterbrochen wurde (in diesem Fall yield num) und führt die Funktion aus, bis ein weiteres yield auftritt oder der Generator beendet wird. Da die nächste Anweisung num = num + 1 ist, aktualisiert er num. Dann geht es an den Anfang der while-Schleife. Die Bedingung ist immer noch wahr. Er trifft auf die nächste Zeile yield num. Sie gibt die aktualisierte num aus und unterbricht. Das geht so lange, wie du willst.

Generatoren als Beobachter

Generatoren können auch Werte mit der Funktion next(val) empfangen. Dann wird der Generator als Beobachter bezeichnet, denn er wacht auf, wenn er neue Werte erhält. In gewissem Sinne hält er Ausschau nach Werten und handelt, wenn er einen erhält. Mehr über dieses Muster können Sie hier nachlesen.

Vorteile von Generatoren

Wie das Beispiel der unendlichen Datenströme zeigt, ist dies nur aufgrund der „Lazy Evaluation“ möglich. Lazy Evaluation ist ein Evaluierungsmodell, das die Evaluierung eines Ausdrucks so lange verzögert, bis sein Wert benötigt wird. Das heißt, wenn wir den Wert nicht brauchen, existiert er nicht. Er wird berechnet, wenn wir ihn brauchen. Sehen wir uns ein Beispiel an –

function * powerSeries(number, power) {
let base = number;
while(true) {
yield Math.pow(base, power);
base++;
}
}

Das powerSeries gibt die Potenzreihe der Zahl an. Zum Beispiel wäre die Potenzreihe von 3 erhöht auf 2 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). Mit const powersOf2 = powerSeries(3, 2); erzeugen wir nur das Generatorobjekt. Keiner der Werte wurde berechnet. Wenn wir nun next() aufrufen, würde 9 berechnet und zurückgegeben.

Speichereffizient

Eine direkte Folge von Lazy Evaluation ist, dass Generatoren speichereffizient sind. Wir generieren nur die Werte, die benötigt werden. Bei normalen Funktionen mussten wir alle Werte im Voraus generieren und sie für den Fall aufbewahren, dass wir sie später brauchen. Mit Generatoren können wir jedoch die Berechnung aufschieben, bis wir sie brauchen.

Wir können Kombinatorfunktionen erstellen, die auf Generatoren wirken. Kombinatoren sind Funktionen, die bestehende Iterabilien kombinieren, um neue zu erzeugen.take ist ein solcher Kombinator. Er nimmt die ersten n Elemente einer Iterablen. Hier ist eine Implementierung –

function * take(n, iter) {
let index = 0;
for (const val of iter) {
if (index >= n) {
return;
}
index = index + 1;
yield val;
}
}

Hier sind einige interessante Anwendungsfälle von take

take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49

Hier ist eine Implementierung einer zyklischen Bibliothek (ohne die Umkehrfunktionalität).

function * cycled(iter) {
const arrOfValues =
while (true) {
for (const val of arrOfValues) {
yield val
}
}
}console.log(...take(10, cycled(take(3, naturalNumbers()))))// 1 2 3 1 2 3 1 2 3 1

Caveats

Es gibt einige Punkte, die man bei der Programmierung mit Generatoren beachten sollte.

  • Generatorobjekte sind nur einmalig zugänglich. Wenn Sie alle Werte ausgeschöpft haben, können Sie sie nicht noch einmal durchlaufen. Um die Werte erneut zu generieren, müssen Sie ein neues Generator-Objekt erstellen.
const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
console.log(...take(10, numbers)) // This will not give any data
  • Generator-Objekte erlauben keinen zufälligen Zugriff, wie es bei Arrays möglich ist. Da die Werte einer nach dem anderen generiert werden, würde der Zugriff auf einen Zufallswert zur Berechnung der Werte bis zu diesem Element führen. Es handelt sich also nicht um einen zufälligen Zugriff.

Fazit

Es gibt noch viele Dinge, die in Generatoren abgedeckt werden müssen. Dinge wie yield *, return() und throw(). Generatoren machen auch Coroutinen möglich. Ich habe einige Referenzen aufgelistet, die Sie lesen können, um ein besseres Verständnis von Generatoren zu erlangen.

Sie können zu Pythons itertools-Seite gehen und einige der Dienstprogramme sehen, die die Arbeit mit Iteratoren und Generatoren ermöglichen. Als Übung können Sie die Hilfsprogramme auch selbst implementieren.

admin

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

lg