Die Autorin wählte den Open Internet/Free Speech Fund, um eine Spende im Rahmen des Programms Write for DOnations zu erhalten.
Im ECMAScript 2015 wurden Generatoren in die JavaScript-Sprache eingeführt. Ein Generator ist ein Prozess, der angehalten und wieder aufgenommen und mehrere Werte liefern kann. Ein Generator in JavaScript besteht aus einer Generator-Funktion, die ein iterierbares Generator
-Objekt zurückgibt.
Generatoren können den Zustand aufrechterhalten, was eine effiziente Methode zur Erstellung von Iteratoren darstellt. Sie sind in der Lage, mit unendlichen Datenströmen umzugehen, was zur Implementierung von infinitem Scrollen auf dem Frontend einer Webanwendung zum Betrieb mit Schallwellendaten und mehr verwendet werden kann. Außerdem können Generatoren, die mit sogenannten Promises (Versprechen) verwendet werden, die async/await
-Funktionalität imitieren, was es uns ermöglicht, mit asynchronem Code auf einer direkteren und lesbaren Weise umzugehen. Obwohl async/await
eine häufigere Methode ist, mit gebräuchlichen, einfachen asynchronen Anwendungsfällen wie dem Abrufen von Daten von einer API umzugehen, verfügen Generatoren über fortgeschrittenere Funktionen, die es lohnenswert machen, ihre Verwendung zu erlernen.
Dieser Artikel behandelt die Erstellung von Generator-Funktionen, die Iterierung über Generator
-Objekten, den Unterschied zwischen yield
und return
innerhalb eines Generators sowie andere Aspekte bezüglich des Arbeitens mit Generatoren.
Eine Generator-Funktion ist eine Funktion, die ein Generator
-Objekt zurückgibt. Sie wird definiert durch das Schlüsselwort function
gefolgt von einem Asterisk (*
), so wie nachstehend gezeigt:
// Generator function declaration
function* generatorFunction() {}
Gelegentlich befindet sich der Asterisk neben dem Funktionsnamen, im Gegensatz zum Schlüsselwort, wie z. B. function *generatorFunction()
. Das funktioniert genauso, aber function*
ist eine allgemein bekanntere Syntax.
Generator-Funktionen können auch in einer Expression definiert sein, wie reguläre Funktionen:
// Generator function expression
const generatorFunction = function*() {}
Generatoren können sogar die Methoden von object und class sein:
// Generator as the method of an object
const generatorObj = {
*generatorMethod() {},
}
// Generator as the method of a class
class GeneratorClass {
*generatorMethod() {}
}
Die Beispiele in diesem Artikel verwenden die Deklarationssyntax von Generator-Funktionen.
Anmerkung: Im Gegensatz zu regulären Funktionen können Generatoren weder mit dem Schlüsselwort new
konstruiert noch in Verbindung mit Pfeilfunktionen verwendet werden.
Nachdem Sie nun wissen, wie Sie Generator-Funktionen deklarieren, sehen Sie sich die iterierbaren Generator
-Objekte an, die diese zurückgeben.
Traditionell laufen Funktionen in JavaScript bis zum Abschluss; das Aufrufen einer Funktion gibt einen Wert zurück, wenn sie beim Schlüsselwort return
ankommt. Wenn das Schlüsselwort return
ausgelassen wird, wird eine Funktion implizit als undefined
zurückgegeben.
Im folgenden Code z. B. deklarieren wir eine sum()
-Funktion, die einen Wert zurückgibt, der die Summe von zwei ganzzahligen Argumenten ist:
// A regular function that sums two values
function sum(a, b) {
return a + b
}
Das Aufrufen der Funktion gibt einen Wert zurück, der die Summe der Argumente ist:
const value = sum(5, 6) // 11
Eine Generator-Funktion hingegen gibt 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 einer Standardfunktion:
// Declare a generator function with a single return value
function* generatorFunction() {
return 'Hello, Generator!'
}
Wenn wir die Generator-Funktion aufrufen, gibt sie das Generator
-Objekt zurück, das wir einer Variable zuweisen können:
// Assign the Generator object to generator
const generator = generatorFunction()
Wenn es eine reguläre Funktion wäre, würden wir erwarten, dass uns generator
die in der Funktion zurückgegebene Zeichenfolge angibt. Was wir jedoch tatsächlich erhalten, ist ein Objekt im Zustand suspended
. Das Aufrufen von generator
ergibt daher ein Output, das dem Folgenden ähnelt:
OutputgeneratorFunction {<suspended>}
__proto__: Generator
[[GeneratorLocation]]: VM272:1
[[GeneratorStatus]]: "suspended"
[[GeneratorFunction]]: ƒ* generatorFunction()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
Das von der Funktion zurückgegebene Generator
-Objekt ist ein Iterator. Ein Iterator ist ein Objekt, das über eine next()
-Methode verfügt, die dazu dient, durch eine Sequenz von Werten zu iterieren. Die next()
-Methode gibt ein Objekt mit value
- und done
-Eigenschaften zurück. value
repräsentiert den zurückgegebenen Wert und done
zeigt an, ob der Iterator alle seine Werte durchlaufen hat oder nicht.
Mit diesem Wissen rufen wir nur next()
auf unserem generator
auf und erhalten den aktuellen Wert und den Zustand des Iterators:
// Call the next method on the Generator object
generator.next()
Dadurch erhalten wir folgendes Output:
Output{value: "Hello, Generator!", done: true}
Der durch das Aufrufen von next()
zurückgegebene Wert ist Hello, Generator!
und der Zustand von done
ist true
, da dieser Wert von einem return
stammt, das den Iterator ausgeschlossen hat. Da der Iterator ausgeführt ist, ändert sich der Status der Generator-Funktion von suspended
zu closed
. Ein erneutes Aufrufen von generator
ergibt Folgendes:
OutputgeneratorFunction {<closed>}
Bisher haben wir nur gezeigt, dass eine Generator-Funktion eine komplexere Methode sein kann, um den return
-Wert einer Funktion zu erhalten. Aber Generator-Funktionen verfügen auch über einzigartige Eigenschaften, die sie von normalen Funktionen unterscheiden. Im nächsten Abschnitt erfahren wir mehr über den yield
-Operator und sehen, wie ein Generator die Ausführung anhalten und wieder aufnehmen kann.
yield
-OperatorenGeneratoren implementieren ein neues Schlüsselwort in JavaScript: yield
.yield
kann eine Generator-Funktion anhalten und den Wert, der auf yield
folgt, zurückgeben. Es bietet somit eine einfache Methode, durch Werte zu iterieren.
In diesem Beispiel halten wir die Generator-Funktion dreimal mit verschiedenen Werten an und geben am Ende einen Wert zurück. Anschließend weisen wir unser Generator
-Objekt der generator
-Variablen zu.
// Create a generator function with multiple yields
function* generatorFunction() {
yield 'Neo'
yield 'Morpheus'
yield 'Trinity'
return 'The Oracle'
}
const generator = generatorFunction()
Wenn wir nun next()
in der Generator-Funktion aufrufen, wird sie jedes Mal anhalten, wenn sie auf yield
trifft. done
wird auf false
gesetzt nach jedem yield
, was anzeigt, dass der Generator nicht abgeschlossen hat. Trifft sie auf ein return
oder wenn keine yield
s mehr in der Funktion zu finden sind, wechselt done
auf true
und der Generator schließt ab.
Verwenden Sie die next()
-Methode viermal in einer Reihe:
// Call next four times
generator.next()
generator.next()
generator.next()
generator.next()
Dies ergibt die folgenden vier Zeilen an Output 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; bei dessen Auslassung gibt die letzte Iteration {value: undefined, done: true}
zurück, ebenso wie etwaige weitere Anrufe an next()
, nachdem ein Generator abgeschlossen hat.
Mithilfe der next()
-Methode haben wir manuell über das Generator
-Objekt iteriert und dabei alle value
- und done
-Eigenschaften des kompletten Objekts erhalten. Doch so wie Array
, Map
und Set
folgt ein Generator
dem Iterationsprotokoll und kann durch for...of
iteriert werden:
// Iterate over Generator object
for (const value of generator) {
console.log(value)
}
Dadurch erhalten wir:
OutputNeo
Morpheus
Trinity
Auch der Spread-Operator kann dazu verwendet werden, die Werte eines Generators
einem Array zuzuweisen.
// Create an array from the values of a Generator object
const values = [...generator]
console.log(values)
Dadurch ergibt sich das folgende Array:
Output(3) ["Neo", "Morpheus", "Trinity"]
Sowohl Spread als auch for...of
beziehen nicht das return
in die Werte mit ein (in diesem Fall wäre das Ergebnis 'The Oracle'
).
Anmerkung: Während beide Methoden wirksam sind, um mit finiten Generatoren zu arbeiten, ist es nicht möglich, spread oder for...of
direkt zu verwenden, ohne eine unendliche Schleife zu erzeugen, wenn ein Generator mit einem infiniten Datenstrom umgeht.
Wie wir sehen, kann in einem Generator beim Iterieren durch seine Werte sein done
-Merkmal auf true
und sein Zustand auf closed
gesetzt sein. Es gibt zwei zusätzliche Möglichkeiten, um einen Generator sofort zu annullieren: mit der return()
-Methode und mit der throw()
-Methode.
Mit return()
kann der Generator an jedem Punkt beendet werden, so wie es bei einem return
-Statement im Funktionskörper der Fall wäre. Sie können ein Argument in return()
einfügen oder es für einen undefinierten Wert freihalten.
Um return()
anzuwenden, 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()
ergibt 'Neo'
, mit done
auf false
gesetzt. Wenn wir direkt danach eine return()
-Methode auf das Generator
-Objekt anwenden, erhalten wir nun den übergebenen Wert und done
ist auf true
gesetzt. Jeder zusätzliche Aufruf an next()
ergibt die Standardantwort Generator abgeschlossen, 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()
Dadurch ergeben sich diese drei Ergebnisse:
Output{value: "Neo", done: false}
{value: "There is no spoon!", done: true}
{value: undefined, done: true}
Die return()
-Methode hat das Generator
-Objekt zum Abschließen und zum Ignorieren von etwaigen anderen yield
-Schlüsselwörtern gezwungen. Dies ist besonders nützlich, wenn Sie bei der asynchronen Programmierung Funktionen annullierbar machen müssen – z. B. das Unterbrechen einer Web-Anfrage, wenn ein Benutzer eine andere Aktion ausführen möchte – da es nicht möglich ist, ein Promise direkt zu annullieren.
Wenn der Körper einer Generator-Funktion eine Möglichkeit hat, Fehler abzufangen und mit ihnen umzugehen, können Sie die throw()
-Methode verwenden, um einen Fehler in den Generator zu injizieren. Das startet den Generator, injiziert den Fehler und beendet den Generator.
Um dies zu demonstrieren, geben wir ein try...catch
in den Körper der Generator-Funktion ein und zeichnen einen Fehler auf, wenn dieser gefunden wird:
// Define a generator function with a try...catch
function* generatorFunction() {
try {
yield 'Neo'
yield 'Morpheus'
} catch (error) {
console.log(error)
}
}
// Invoke the generator and throw an error
const generator = generatorFunction()
Jetzt führen wir die next()
-Methode aus, gefolgt von throw()
:
generator.next()
generator.throw(new Error('Agent Smith!'))
Dadurch erhalten wir folgendes Output:
Output{value: "Neo", done: false}
Error: Agent Smith!
{value: undefined, done: true}
Wir haben mit throw()
einen Fehler in den Generator injiziert, der vom try...catch
aufgefangen und in der Konsole aufgezeichnet wurde.
Die folgende Tabelle zeigt eine Liste von Methoden, die auf Generator
-Objekte angewendet werden können:
Methode | Beschreibung |
---|---|
next() |
Gibt den nächsten Wert in einem Generator zurück |
return() |
Gibt einen Wert in einem Generator zurück und beendet den Generator |
throw() |
Injiziert einen Fehler und beendet den Generator |
Die nächste Tabelle listet die möglichen Zustände eines Generator
-Objekts auf:
Zustand | Beschreibung |
---|---|
suspended |
Der Generator hat die Ausführung angehalten, ist aber nicht beendet |
closed |
Der Generator wurde beendet, indem er entweder auf einen Fehler gestoßen, zurückgekehrt oder durch alle Werte iteriert ist |
yield
DelegationAußer dem regulären yield
-Operator können Generatoren auch die yield*
-Expression verwenden, um weitere Werte an einen anderen Generator zu delegieren. Wenn yield*
in einem Generator auftritt, geht es in den delegierten Generator und beginnt, durch alle yield
s zu iterieren, bis der Generator beendet ist. Dies kann dazu genutzt werden, verschiedene Generator-Funktionen zu trennen, um Ihren Code semantisch zu organisieren, während alle ihre yield
s in der richtigen Reihenfolge iterierbar bleiben.
Um dies zu demonstrieren, können wir zwei Generator-Funktionen erstellen, von denen eine eine yield*
-Operation auf die andere ausführt:
// Generator function that will be delegated to
function* delegate() {
yield 3
yield 4
}
// Outer generator function
function* begin() {
yield 1
yield 2
yield* delegate()
}
Als Nächstes iterieren wir durch die Generator-Funktion begin()
:
// Iterate through the outer generator
const generator = begin()
for (const value of generator) {
console.log(value)
}
Dadurch erhalten wir folgende Werte in der Reihenfolge, in der sie generiert werden:
Output1
2
3
4
Der äußere Generator lieferte die Werte 1
und 2
, delegierte dann an den anderen Generator mit yield*
, der 3
und 4
zurückgab.
yield*
kann auch an jedes Objekt delegieren, das iterierbar ist, wie z. B. ein Array oder ein Map. Die Yield-Delegation kann bei der Organisation von Code hilfreich sein, da jede Funktion innerhalb eines Generators, die yield
verwenden will, auch ein Generator sein muss.
Eine der nützlichen Aspekte von Generatoren ist die Fähigkeit, mit unendlichen Datenströmen und -sammlungen zu arbeiten. Dies kann durch das Erstellen einer Endlosschleife innerhalb einer Generator-Funktion demonstriert werden, die eine Zahl um eins erhöht.
Im folgenden Code-Block definieren wir diese Generator-Funktion und initiieren dann den Generator:
// Define a generator function that increments by one
function* incrementer() {
let i = 0
while (true) {
yield i++
}
}
// Initiate the generator
const counter = incrementer()
Iterieren Sie nun durch die Werte mit next()
:
// Iterate through the values
counter.next()
counter.next()
counter.next()
counter.next()
Dadurch erhalten Sie folgendes Output:
Output{value: 0, done: false}
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
Die Funktion gibt in der Endlosschleife aufeinanderfolgende Werte zurück, während das done
-Merkmal auf false
bleibt, wodurch sichergestellt wird, dass der Vorgang nicht beendet wird.
Mit Generatoren müssen Sie sich nicht um die Erstellung einer Endlosschleife kümmern, da Sie die Ausführung nach Belieben anhalten und wieder aufnehmen können. Sie müssen jedoch immer noch vorsichtig sein, wie Sie den Generator aufrufen. Wenn Sie Spread oder for...of
bei einem infiniten Datenstrom anwenden, iterieren Sie plötzlich immer noch über eine Endlosschleife, was zum Absturz der Umgebung führt.
Für ein komplexeres Beispiel eines infiniten Datenstroms können wir eine Fibonacci-Generator-Funktion erstellen. Die Fibonacci-Sequenz, die die beiden vorherigen Werte kontinuierlich zusammenrechnet, kann mit einer Endlosschleife innerhalb eines Generators wie folgt geschrieben werden:
// Create a fibonacci generator function
function* fibonacci() {
let prev = 0
let next = 1
yield prev
yield next
// Add previous and next values and yield them forever
while (true) {
const newVal = next + prev
yield newVal
prev = next
next = newVal
}
}
Um dies zu testen, können wir für eine Endlosschleife eine endliche Zahl verwenden und die Fibonacci-Sequenz in die Konsole eingeben.
// Print the first 10 values of fibonacci
const fib = fibonacci()
for (let i = 0; i < 10; i++) {
console.log(fib.next().value)
}
Dadurch ergibt sich Folgendes:
Output0
1
1
2
3
5
8
13
21
34
Die Fähigkeit, mit unendlichen Datensätzen zu arbeiten, ist ein Teil dessen, was Generatoren so leistungsstark macht. Dies kann nützlich sein für Beispiele wie infinites Scrollen auf dem Frontend einer Webanwendung.
In diesem Artikel haben wir Generatoren als Iteratoren verwendet und Werte in jeder Iteration geliefert. Neben der Erzeugung von Werten können Generatoren auch Werte aus next()
konsumieren. In diesem Fall beinhaltet yield
einen Wert.
Es ist wichtig zu beachten, dass das erste aufgerufene next()
keinen Wert übergibt, sondern nur den Generator startet. Um das zu demonstrieren, können wir den Wert von yield
aufzeichnen und einige Male next()
mit einigen Werten aufrufen.
function* generatorFunction() {
console.log(yield)
console.log(yield)
return 'The end'
}
const generator = generatorFunction()
generator.next()
generator.next(100)
generator.next(200)
Dadurch ergibt sich folgendes Output:
Output100
200
{value: "The end", done: true}
Es ist auch möglich, den Generator mit einem Anfangswert zu versehen. Im folgenden Beispiel erstellen wir eine for
-Schleife und übergeben jeden Wert in die next()
-Methode, versehen die ürsprüngliche Funktion aber auch mit einem Argument:
function* generatorFunction(value) {
while (true) {
value = yield value * 10
}
}
// Initiate a generator and seed it with an initial value
const generator = generatorFunction(0)
for (let i = 0; i < 5; i++) {
console.log(generator.next(i).value)
}
Wir rufen den Wert von next()
ab und geben der nächsten Iteration einen neuen Wert, der dem vorherigen Wert plus Zehn entspricht. Dadurch ergibt sich Folgendes:
Output0
10
20
30
40
Eine weitere Möglichkeit des Startens eines Generators besteht darin, den Generator mit einer Funktion zu umgeben, die vor jeder anderen Handlung immer einmal next()
anruft.
async
/await
mit GeneratorenEine asynchrone Funktion ist ein in ES6+ JavaScript verfügbarer Funktionstyp, der das Arbeiten mit asynchronen Daten erleichtert, indem er diese synchron erscheinen lässt. Generatoren verfügen über eine umfangreichere Palette von Fähigkeiten als asynchrone Funktionen, sind jedoch in der Lage, ein ähnliches Verhalten zu replizieren. Die Implementierung asynchroner Programmierung auf diese Weise kann die Flexibilität Ihres Codes erhöhen.
In diesem Abschnitt zeigen wir ein Beispiel zur Reproduktion von async
/await
mit Generatoren.
Wir erstellen eine asynchrone Funktion, die die Fetch API verwendet, um Daten über die JSONPlaceholder API (die JSON-Beispieldaten zu Testzwecken bereitstellt) zu erhalten, und die Rückmeldung in der Konsole aufzeichnet.
Beginnen Sie mit dem Definieren einer asynchronen Funktion namens getUsers
, die Daten von der API abruft und eine Reihe von Objekten zurückgibt, und rufen Sie dann getUsers
auf:
const getUsers = async function() {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const json = await response.json()
return json
}
// Call the getUsers function and log the response
getUsers().then(response => console.log(response))
Dadurch ergeben sich JSON-Daten, die ähnlich sind wie folgt:
Output[ {id: 1, name: "Leanne Graham" ...},
{id: 2, name: "Ervin Howell" ...},
{id: 3, name": "Clementine Bauch" ...},
{id: 4, name: "Patricia Lebsack"...},
{id: 5, name: "Chelsey Dietrich"...},
...]
Mit Generatoren können wir etwas nahezu Identisches erstellen, das die Schlüsselwörter async
/await
nicht verwendet. Es verwendet stattdessen eine neue, von uns erstellte Funktion und liefert yield
-Werte anstatt von await
-Promises.
Im folgenden Code-Block definieren wir eine Funktion namens getUsers
, die unsere neue asyncAlt
-Funktion (wir schreiben diese später) nutzt, um async
/await
zu imitieren.
const getUsers = asyncAlt(function*() {
const response = yield fetch('https://jsonplaceholder.typicode.com/users')
const json = yield response.json()
return json
})
// Invoking the function
getUsers().then(response => console.log(response))
Wie wir sehen können, sieht diese fast genauso aus wie die async
/await
-Implementierung – außer dass eine Generator-Funktion vorhanden ist, die Werte liefert.
Nun können wir eine asyncAlt
-Funktion erstellen, die einer asynchronen Funktion ähnelt. asyncAlt
hat eine Generator-Funktion als Parameter – das ist unsere Funktion, die die Promises liefert, die fetch
zurückgibt. asyncAlt
gibt selbst eine Funktion zurück und löst jedes Promise, das es findet, bis hin zum letzten:
// Define a function named asyncAlt that takes a generator function as an argument
function asyncAlt(generatorFunction) {
// Return a function
return function() {
// Create and assign the generator object
const generator = generatorFunction()
// Define a function that accepts the next iteration of the generator
function resolve(next) {
// If the generator is closed and there are no more values to yield,
// resolve the last value
if (next.done) {
return Promise.resolve(next.value)
}
// If there are still values to yield, they are promises and
// must be resolved.
return Promise.resolve(next.value).then(response => {
return resolve(generator.next(response))
})
}
// Begin resolving promises
return resolve(generator.next())
}
}
Das ergibt dasselbe Output wie bei der async
/await
-Version:
Output[ {id: 1, name: "Leanne Graham" ...},
{id: 2, name: "Ervin Howell" ...},
{id: 3, name": "Clementine Bauch" ...},
{id: 4, name: "Patricia Lebsack"...},
{id: 5, name: "Chelsey Dietrich"...},
...]
Beachten Sie, dass diese Implementierung demonstriert, wie Generatoren anstelle von async
/await
verwendet werden können. Sie ist jedoch kein produktreifer Entwurf. Sie verfügt weder über eine Einrichtung zur Fehlerbehandlung noch über die Fähigkeit, Parameter in die gelieferten Werte zu übergeben. Obwohl diese Methode Ihrem Code mehr Flexibilität verleihen kann, ist async/await
oft die bessere Wahl, da es Implementierungsdetails abstrahiert, sodass Sie sich auf das Schreiben von produktivem Code konzentrieren können.
Generatoren sind Prozesse, die Ausführungen anhalten und wieder aufnehmen können. Sie sind ein leistungsfähiger, vielseitiger Bestandteil von JavaScript, obwohl sie nicht häufig verwendet werden. In diesem Tutorial haben Sie mehr über Generator-Funktionen und Generator-Objekte, für Generatoren verfügbare Methoden, yield
- und yield*
-Operatoren sowie Generatoren mit finiten und infiniten Datensätzen erfahren. Behandelt wurde auch eine Möglichkeit, asynchronen Code ohne geschachtelte Callbacks oder lange Promise-Ketten zu implementieren.
Wenn Sie mehr über die Syntax von JavaScript lernen möchten, besuchen Sie unsere Tutorials Grundlegendes zu This, Bind, Call und Apply in JavaScript sowie Grundlegendes zu den Objekten Map und Set in JavaScript.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!