Understanding Generators in JavaScript

Der Autor hat den Open Internet/Free Speech Fund ausgewählt, um eine Spende im Rahmen des Write for DOnations-Programms zu erhalten.

Einführung

In ECMAScript 2015 wurden Generatoren in die JavaScript-Sprache eingeführt. Ein Generator ist ein Prozess, der angehalten und wieder fortgesetzt werden kann und mehrere Werte liefern kann. Ein Generator in JavaScript besteht aus einer Generatorfunktion, die ein iterierbares Generator Objekt zurückgibt.

Generatoren können einen Zustand beibehalten, was eine effiziente Methode zur Erstellung von Iteratoren darstellt, und sind in der Lage, mit unendlichen Datenströmen umzugehen, was zur Implementierung eines unendlichen Bildlaufs auf dem Frontend einer Webanwendung, zur Verarbeitung von Schallwellendaten und vielem mehr verwendet werden kann. Darüber hinaus können Generatoren in Verbindung mit Promises die async/await-Funktionalität imitieren, was den Umgang mit asynchronem Code auf einfachere und lesbarere Weise ermöglicht. Obwohl async/await ein gängiger Weg ist, um mit häufigen, einfachen asynchronen Anwendungsfällen umzugehen, wie z. B. dem Abrufen von Daten von einer API, haben Generatoren fortgeschrittenere Funktionen, die es lohnenswert machen, ihre Verwendung zu erlernen.

In diesem Artikel werden wir behandeln, wie man Generatorfunktionen erstellt, wie man über Generator-Objekte iteriert, den Unterschied zwischen yield und return innerhalb eines Generators und andere Aspekte der Arbeit mit Generatoren.

Generatorfunktionen

Eine Generatorfunktion ist eine Funktion, die ein Generator-Objekt zurückgibt und durch das Schlüsselwort function, gefolgt von einem Sternchen (*), definiert wird, wie im Folgenden gezeigt:

// Generator function declarationfunction* generatorFunction() {}

Gelegentlich sehen Sie das Sternchen neben dem Funktionsnamen, im Gegensatz zum Funktionsschlüsselwort, wie function *generatorFunction(). Das funktioniert genauso, aber function* ist eine allgemein akzeptierte Syntax.

Generatorfunktionen können auch in einem Ausdruck definiert werden, wie reguläre Funktionen:

// Generator function expressionconst generatorFunction = function*() {}

Generatoren können sogar die Methoden eines Objekts oder einer Klasse sein:

Die Beispiele in diesem Artikel verwenden die Syntax der Generatorfunktionsdeklaration.

Hinweis: Im Gegensatz zu regulären Funktionen können Generatoren nicht mit dem Schlüsselwort new konstruiert werden, und sie können auch nicht in Verbindung mit Pfeilfunktionen verwendet werden.

Nachdem Sie nun wissen, wie man Generatorfunktionen deklariert, lassen Sie uns einen Blick auf die iterierbaren Generator Objekte werfen, die sie zurückgeben.

Generatorobjekte

Traditionell laufen Funktionen in JavaScript bis zum Ende, und der Aufruf einer Funktion gibt einen Wert zurück, wenn er am Schlüsselwort return ankommt. Wenn das Schlüsselwort return weggelassen wird, gibt eine Funktion implizit undefined zurück.

Im folgenden Code wird zum Beispiel eine sum()-Funktion deklariert, die einen Wert zurückgibt, der die Summe zweier ganzzahliger Argumente ist:

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

Der Aufruf der Funktion gibt einen Wert zurück, der die Summe der Argumente ist:

const value = sum(5, 6) // 11

Eine Generatorfunktion gibt jedoch nicht sofort einen Wert zurück, sondern ein iterierbares Generator-Objekt. Im folgenden Beispiel deklarieren wir eine Funktion und geben ihr einen einzelnen Rückgabewert, wie eine Standardfunktion:

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

Wenn wir die Generatorfunktion aufrufen, gibt sie das Objekt Generator zurück, das wir einer Variablen zuweisen können:

// Assign the Generator object to generatorconst generator = generatorFunction()

Wenn dies eine reguläre Funktion wäre, würden wir erwarten, dass generator uns die in der Funktion zurückgegebene Zeichenfolge liefert. Was wir jedoch tatsächlich erhalten, ist ein Objekt im Zustand suspended. Der Aufruf von generator ergibt daher eine Ausgabe ähnlich der folgenden:

Das von der Funktion zurückgegebene Generator Objekt ist ein Iterator. Ein Iterator ist ein Objekt, das über eine next()-Methode verfügt, die für die Iteration durch eine Folge von Werten verwendet wird. Die Methode next() gibt ein Objekt mit den Eigenschaften value und done zurück. value steht für den zurückgegebenen Wert, und done gibt an, ob der Iterator alle Werte durchlaufen hat oder nicht.

Da wir das wissen, rufen wir next() auf unserem generator auf und erhalten den aktuellen Wert und den Status des Iterators:

// Call the next method on the Generator objectgenerator.next()

Das ergibt die folgende Ausgabe:

Output
{value: "Hello, Generator!", done: true}

Der vom Aufruf von next() zurückgegebene Wert ist Hello, Generator!, und der Status von done ist true, weil dieser Wert von einem return stammt, der den Iterator beendet hat. Da der Iterator beendet ist, ändert sich der Status der Generatorfunktion von suspended auf closed. Ein erneuter Aufruf von generator ergibt Folgendes:

Output
generatorFunction {<closed>}

Bis jetzt haben wir nur demonstriert, wie eine Generatorfunktion eine komplexere Methode sein kann, um den return-Wert einer Funktion zu erhalten. Aber Generatorfunktionen haben auch einzigartige Eigenschaften, die sie von normalen Funktionen unterscheiden. Im nächsten Abschnitt lernen wir den yield-Operator kennen und sehen, wie ein Generator die Ausführung anhalten und fortsetzen kann.

yield-Operatoren

Generatoren führen ein neues Schlüsselwort in JavaScript ein: yield. yield kann eine Generatorfunktion anhalten und den Wert zurückgeben, der auf yield folgt, und bietet so eine leichtgewichtige Möglichkeit, durch Werte zu iterieren.

In diesem Beispiel werden wir die Generatorfunktion dreimal mit verschiedenen Werten anhalten und am Ende einen Wert zurückgeben. Dann weisen wir unser Objekt Generator der Variablen generator zu.

Wenn wir nun die Generatorfunktion next() aufrufen, hält sie jedes Mal an, wenn sie auf yield trifft. done wird nach jedem yield auf false gesetzt, was anzeigt, dass der Generator noch nicht fertig ist. Sobald er auf return trifft oder keine weiteren yield in der Funktion vorkommen, wechselt done zu true und der Generator ist beendet.

Verwenden Sie die Methode next() viermal hintereinander:

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

Das ergibt die folgenden vier Zeilen der Ausgabe in der Reihenfolge:

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

Beachten Sie, dass ein Generator kein return benötigt; wenn es weggelassen wird, gibt die letzte Iteration {value: undefined, done: true} zurück, ebenso wie alle nachfolgenden Aufrufe von next(), nachdem ein Generator beendet wurde.

Iterieren über einen Generator

Mit der Methode next() haben wir das Generator-Objekt manuell durchlaufen und dabei alle value– und done-Eigenschaften des vollständigen Objekts erhalten. Aber genau wie Array, Map und Set folgt ein Generator dem Iterationsprotokoll und kann mit for...of durchlaufen werden:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

Dies ergibt folgendes:

Output
NeoMorpheusTrinity

Der Spread-Operator kann auch verwendet werden, um die Werte eines Generator einem Array zuzuweisen.

// Create an array from the values of a Generator objectconst values = console.log(values)

Das ergibt das folgende Array:

Output
(3)

Beide, Spreizung und for...of, faktorisieren das return nicht in die Werte (in diesem Fall wäre es 'The Oracle').

Hinweis: Während diese beiden Methoden für die Arbeit mit endlichen Generatoren effektiv sind, ist es bei einem Generator mit einem unendlichen Datenstrom nicht möglich, spread oder for...of direkt zu verwenden, ohne eine Endlosschleife zu erzeugen.

Generator schließen

Wie wir gesehen haben, kann ein Generator seine done-Eigenschaft auf true und seinen Status auf closed setzen, indem er durch alle seine Werte iteriert. Es gibt zwei weitere Möglichkeiten, einen Generator sofort abzubrechen: mit der Methode return() und mit der Methode throw().

Mit return() kann der Generator an jedem beliebigen Punkt abgebrochen werden, so als ob eine return-Anweisung im Funktionskörper gestanden hätte. Sie können ein Argument an return() übergeben oder es für einen undefinierten Wert leer lassen.

Um return() zu demonstrieren, erstellen wir einen Generator mit einigen yield-Werten, aber ohne return in der Funktionsdefinition:

function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()

Das erste next() gibt uns 'Neo', wobei done auf false gesetzt ist. Wenn wir direkt danach eine return()-Methode für das Generator-Objekt aufrufen, erhalten wir den übergebenen Wert und done wird auf true gesetzt. Jeder weitere Aufruf von next() ergibt die Standardantwort des abgeschlossenen Generators mit einem undefinierten Wert.

Um dies zu demonstrieren, führen Sie die folgenden drei Methoden auf generator aus:

generator.next()generator.return('There is no spoon!')generator.next()

Das ergibt die drei folgenden Ergebnisse:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

Die return()-Methode zwingt das Generator-Objekt zur Vervollständigung und zum Ignorieren aller anderen yield-Schlüsselwörter. Dies ist besonders nützlich in der asynchronen Programmierung, wenn Sie Funktionen abbrechbar machen müssen, wie z.B. das Unterbrechen einer Web-Anfrage, wenn ein Benutzer eine andere Aktion durchführen möchte, da es nicht möglich ist, ein Promise direkt abzubrechen.

Wenn der Körper einer Generatorfunktion eine Möglichkeit hat, Fehler abzufangen und zu behandeln, können Sie die throw()-Methode verwenden, um einen Fehler in den Generator zu werfen. Dies startet den Generator, wirft den Fehler hinein und beendet den Generator.

Um dies zu demonstrieren, fügen wir ein try...catch in den Körper der Generatorfunktion ein und protokollieren einen Fehler, wenn einer gefunden wird:

Nun führen wir die Methode next() aus, gefolgt von throw():

generator.next()generator.throw(new Error('Agent Smith!'))

Das ergibt die folgende Ausgabe:

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

Mit throw() haben wir einen Fehler in den Generator eingeschleust, der von try...catch abgefangen und auf der Konsole protokolliert wurde.

Methoden und Zustände von Generatorobjekten

Die folgende Tabelle zeigt eine Liste von Methoden, die auf Generator-Objekte angewendet werden können:

Methode Beschreibung
next() Returnt den nächsten Wert in einem Generator
return() Returnt einen Wert in einem Generator zurück und beendet den Generator
throw() Wirft einen Fehler und beendet den Generator

Die nächste Tabelle listet die möglichen Zustände eines Generator Objekts auf:

Generator hat sich entweder durch einen Fehler beendet, zurückkehrte oder durch alle Werte iterierte

Status Beschreibung
suspended Generator hat die Ausführung angehalten, aber nicht beendet
closed

yield Delegation

Zusätzlich zum regulären yield-Operator können Generatoren auch den Ausdruck yield* verwenden, um weitere Werte an einen anderen Generator zu delegieren. Wenn der yield*-Ausdruck innerhalb eines Generators auftritt, geht er in den delegierten Generator und beginnt mit der Iteration durch alle yields, bis dieser Generator geschlossen ist. Dies kann verwendet werden, um verschiedene Generatorfunktionen zu trennen, um den Code semantisch zu organisieren, während alle ihre yields in der richtigen Reihenfolge iterierbar sind.

Zur Veranschaulichung können wir zwei Generatorfunktionen erstellen, von denen eine yield* auf die andere wirkt:

Als Nächstes iterieren wir durch die begin() Generatorfunktion:

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

Das ergibt die folgenden Werte in der Reihenfolge, in der sie generiert werden:

Output
1234

Der äußere Generator liefert die Werte 1 und 2, delegiert dann an den anderen Generator mit yield*, der 3 und 4 liefert.

yield* kann auch an ein beliebiges Objekt delegiert werden, das iterierbar ist, z. B. ein Array oder eine Map. Yield-Delegation kann bei der Organisation von Code hilfreich sein, da jede Funktion innerhalb eines Generators, die yield verwenden wollte, auch ein Generator sein müsste.

Unendliche Datenströme

Einer der nützlichen Aspekte von Generatoren ist die Fähigkeit, mit unendlichen Datenströmen und Sammlungen zu arbeiten. Dies kann durch die Erstellung einer Endlosschleife innerhalb einer Generatorfunktion demonstriert werden, die eine Zahl um eins erhöht.

Im folgenden Codeblock definieren wir diese Generatorfunktion und starten dann den Generator:

Jetzt iterieren wir mit next() durch die Werte:

// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()

Das ergibt folgende Ausgabe:

Output
{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}

Die Funktion gibt aufeinanderfolgende Werte in der Endlosschleife zurück, während die done-Eigenschaft false bleibt, um sicherzustellen, dass sie nicht beendet wird.

Mit Generatoren muss man sich keine Sorgen machen, dass eine Endlosschleife entsteht, denn man kann die Ausführung nach Belieben anhalten und fortsetzen. Allerdings muss man trotzdem vorsichtig sein, wie man den Generator aufruft. Wenn Sie spread oder for...of für einen unendlichen Datenstrom verwenden, iterieren Sie immer noch über eine Endlosschleife auf einmal, was zum Absturz der Umgebung führt.

Als komplexeres Beispiel für einen unendlichen Datenstrom können wir eine Fibonacci-Generatorfunktion erstellen. Die Fibonacci-Folge, die kontinuierlich die beiden vorherigen Werte addiert, kann mit einer Endlosschleife innerhalb eines Generators wie folgt geschrieben werden:

Um dies zu testen, können wir eine endliche Zahl in einer Schleife durchlaufen und die Fibonacci-Folge auf der Konsole ausgeben.

// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}

Das ergibt folgendes Ergebnis:

Output
0112358132134

Die Fähigkeit, mit unendlichen Datensätzen zu arbeiten, ist ein Teil dessen, was Generatoren so leistungsfähig macht. Dies kann für Beispiele wie die Implementierung eines unendlichen Bildlaufs im Frontend einer Webanwendung nützlich sein.

Werte in Generatoren weitergeben

In diesem Artikel haben wir Generatoren als Iteratoren verwendet und bei jeder Iteration Werte ausgegeben. Generatoren können nicht nur Werte erzeugen, sondern auch Werte aus next() verbrauchen. In diesem Fall wird yield einen Wert enthalten.

Es ist wichtig zu beachten, dass das erste next(), das aufgerufen wird, keinen Wert übergibt, sondern nur den Generator startet. Um dies zu demonstrieren, können wir den Wert von yield protokollieren und next() ein paar Mal mit einigen Werten aufrufen.

Das ergibt die folgende Ausgabe:

Output
100200{value: "The end", done: true}

Es ist auch möglich, den Generator mit einem Anfangswert zu versehen. Im folgenden Beispiel machen wir eine for-Schleife und übergeben jeden Wert an die next()-Methode, übergeben aber auch ein Argument an die Ausgangsfunktion:

Wir rufen den Wert aus next() ab und geben der nächsten Iteration einen neuen Wert, der dem vorherigen Wert mal zehn entspricht. Das ergibt folgendes:

Output
010203040

Eine andere Möglichkeit, einen Generator zu starten, besteht darin, den Generator in eine Funktion zu verpacken, die next() immer einmal aufruft, bevor sie etwas anderes tut.

async/await with Generators

Eine asynchrone Funktion ist ein Funktionstyp, der in ES6+ JavaScript verfügbar ist und die Arbeit mit asynchronen Daten leichter verständlich macht, indem er sie synchron erscheinen lässt. Generatoren haben einen größeren Funktionsumfang als asynchrone Funktionen, sind aber in der Lage, ein ähnliches Verhalten zu reproduzieren. Die Implementierung asynchroner Programmierung auf diese Weise kann die Flexibilität Ihres Codes erhöhen.

In diesem Abschnitt werden wir ein Beispiel für die Reproduktion von async/await mit Generatoren demonstrieren.

Lassen Sie uns eine asynchrone Funktion erstellen, die die Fetch-API verwendet, um Daten von der JSONPlaceholder-API zu erhalten (die JSON-Beispieldaten für Testzwecke bereitstellt) und die Antwort auf der Konsole protokolliert.

Beginnen Sie mit der Definition einer asynchronen Funktion namens getUsers, die Daten von der API abruft und ein Array von Objekten zurückgibt, und rufen Sie dann getUsers auf:

Dies ergibt JSON-Daten ähnlich wie die folgenden:

Mit Hilfe von Generatoren können wir etwas fast Identisches erstellen, das die Schlüsselwörter async/await nicht verwendet. Stattdessen werden eine neue Funktion, die wir erstellen, und yield-Werte anstelle von await-Versprechen verwendet.

Im folgenden Codeblock definieren wir eine Funktion namens getUsers, die unsere neue asyncAlt-Funktion (die wir später schreiben werden) verwendet, um async/await nachzuahmen.

Wie wir sehen, sieht es fast identisch mit der async/await-Implementierung aus, außer dass eine Generatorfunktion übergeben wird, die Werte liefert.

Nun können wir eine asyncAlt-Funktion erstellen, die einer asynchronen Funktion ähnelt. asyncAlt hat eine Generatorfunktion als Parameter, die unsere Funktion ist, die die Versprechen liefert, die fetch zurückgibt. asyncAlt gibt selbst eine Funktion zurück und löst jedes Versprechen auf, das es findet, bis zum letzten:

Dies ergibt die gleiche Ausgabe wie die async/await-Version:

Beachten Sie, dass diese Implementierung dazu dient, zu demonstrieren, wie Generatoren anstelle von async/await verwendet werden können, und kein produktionsreifes Design ist. Sie verfügt weder über eine Fehlerbehandlung noch über die Möglichkeit, Parameter an die erzeugten Werte zu übergeben. Obwohl diese Methode Ihren Code flexibler machen kann, ist async/await oft die bessere Wahl, da sie die Implementierungsdetails abstrahiert und Sie sich auf das Schreiben von produktivem Code konzentrieren können.

Abschluss

Generatoren sind Prozesse, die die Ausführung anhalten und wieder aufnehmen können. Sie sind eine mächtige, vielseitige Funktion von JavaScript, obwohl sie nicht häufig verwendet werden. In diesem Tutorium haben wir Generatorfunktionen und Generatorobjekte, Methoden für Generatoren, die yield– und yield*-Operatoren und Generatoren für endliche und unendliche Datensätze kennen gelernt. Außerdem haben wir eine Möglichkeit erkundet, asynchronen Code ohne verschachtelte Rückrufe oder lange Versprechungsketten zu implementieren.

Wenn Sie mehr über die JavaScript-Syntax erfahren möchten, sehen Sie sich unsere Tutorials This, Bind, Call und Apply in JavaScript und Map and Set Objects in JavaScript verstehen an.

Schreibe einen Kommentar

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