Synchrone Generatoren sind spezielle Versionen von Funktions- und Methodendefinitionen, die immer synchrone Iterables zurückgeben.
// Generator function declaration
function* genFunc1() { /*···*/ }
// Generator function expression
const genFunc2 = function* () { /*···*/ };
// Generator method definition in an object literal
const obj = {
* generatorMethod() {
// ···
}
};
// Generator method definition in a class definition
// (class declaration or class expression)
class MyClass {
* generatorMethod() {
// ···
}
}Ein Sternchen (*) kennzeichnet Funktionen und Methoden als Generatoren.
function* ist eine Kombination aus dem Schlüsselwort function und einem Sternchen.* ist ein Modifikator (ähnlich wie static und get).yieldWenn wir eine Generatorfunktion aufrufen, gibt sie ein Iterable zurück (genauer gesagt: einen Iterator, der auch selbst ein Iterable ist). Der Generator füllt dieses Iterable über den yield-Operator.
function* genFunc1() {
yield 'a';
yield 'b';
}
const iterable = genFunc1();
// Convert the iterable to an Array, to check what’s inside:
assert.deepEqual(
Array.from(iterable), ['a', 'b']
);
// We can also use a for-of loop
for (const x of genFunc1()) {
console.log(x);
}
// Output:
// 'a'
// 'b'yield pausiert eine GeneratorfunktionDie Verwendung einer Generatorfunktion umfasst die folgenden Schritte:
iter zurück (der auch ein Iterable ist).iter ruft wiederholt iter.next() auf. Jedes Mal springen wir in den Körper der Generatorfunktion, bis ein yield einen Wert zurückgibt.Daher tut yield mehr, als nur Werte zu Iterables hinzuzufügen – es pausiert auch und verlässt die Generatorfunktion.
return beendet ein yield den Funktionskörper und gibt einen Wert zurück (über .next()).return setzt die Ausführung bei einer erneuten Invokation (von .next()) direkt nach dem yield fort.Betrachten wir dies anhand der folgenden Generatorfunktion.
let location = 0;
function* genFunc2() {
location = 1; yield 'a';
location = 2; yield 'b';
location = 3;
}Um genFunc2() zu verwenden, müssen wir zuerst den Iterator/Iterable iter erstellen. genFunc2() ist nun „vor“ seinem Körper pausiert.
const iter = genFunc2();
// genFunc2() is now paused “before” its body:
assert.equal(location, 0);iter implementiert das Iterationsprotokoll. Daher steuern wir die Ausführung von genFunc2() über iter.next(). Das Aufrufen dieser Methode setzt das pausierte genFunc2() fort und führt es aus, bis ein yield erreicht wird. Dann pausiert die Ausführung und .next() gibt den Operanden des yield zurück.
assert.deepEqual(
iter.next(), {value: 'a', done: false});
// genFunc2() is now paused directly after the first `yield`:
assert.equal(location, 1);Beachten Sie, dass der gewertete Wert 'a' in einem Objekt verpackt ist, was die Art und Weise ist, wie Iteratoren ihre Werte immer liefern.
Wir rufen iter.next() erneut auf, und die Ausführung wird dort fortgesetzt, wo wir zuvor pausiert hatten. Sobald wir auf das zweite yield stoßen, wird genFunc2() pausiert und .next() gibt den gewerteten Wert 'b' zurück.
assert.deepEqual(
iter.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the second `yield`:
assert.equal(location, 2);Wir rufen iter.next() ein weiteres Mal auf, und die Ausführung wird fortgesetzt, bis sie den Körper von genFunc2() verlässt.
assert.deepEqual(
iter.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);Diesmal ist die Eigenschaft .done des Ergebnisses von .next() true, was bedeutet, dass der Iterator beendet ist.
yield die Ausführung?Welche Vorteile hat es, dass yield die Ausführung pausiert? Warum funktioniert es nicht einfach wie die Array-Methode .push() und füllt das Iterable mit Werten, ohne zu pausieren?
Durch das Pausieren bieten Generatoren viele Features von Coroutinen (stellen Sie sich Prozesse vor, die kooperativ multitasken). Wenn wir beispielsweise nach dem nächsten Wert eines Iterables fragen, wird dieser Wert lazy (bei Bedarf) berechnet. Die folgenden beiden Generatorfunktionen demonstrieren, was das bedeutet.
/**
* Returns an iterable over lines
*/
function* genLines() {
yield 'A line';
yield 'Another line';
yield 'Last line';
}
/**
* Input: iterable over lines
* Output: iterable over numbered lines
*/
function* numberLines(lineIterable) {
let lineNumber = 1;
for (const line of lineIterable) { // input
yield lineNumber + ': ' + line; // output
lineNumber++;
}
}Beachten Sie, dass das yield in numberLines() innerhalb einer for-of-Schleife vorkommt. yield kann in Schleifen verwendet werden, aber nicht in Callbacks (mehr dazu später).
Kombinieren wir beide Generatoren, um das Iterable numberedLines zu erzeugen.
const numberedLines = numberLines(genLines());
assert.deepEqual(
numberedLines.next(), {value: '1: A line', done: false});
assert.deepEqual(
numberedLines.next(), {value: '2: Another line', done: false});Der Hauptvorteil der Verwendung von Generatoren ist hier, dass alles inkrementell funktioniert: Über numberedLines.next() fragen wir numberLines() nach nur einer nummerierten Zeile. Diese wiederum fragt genLines() nach nur einer unnummerierten Zeile.
Dieser Inkrementalismus funktioniert auch dann weiter, wenn beispielsweise genLines() seine Zeilen aus einer großen Textdatei liest: Wenn wir numberLines() nach einer nummerierten Zeile fragen, erhalten wir eine, sobald genLines() seine erste Zeile aus der Textdatei gelesen hat.
Ohne Generatoren würde genLines() zuerst alle Zeilen lesen und sie zurückgeben. Dann würde numberLines() alle Zeilen nummerieren und sie zurückgeben. Wir müssen daher viel länger warten, bis wir die erste nummerierte Zeile erhalten.
Übung: Eine normale Funktion in einen Generator umwandeln
exercises/sync-generators/fib_seq_test.mjs
Die folgende Funktion mapIter() ähnelt der Array-Methode .map(), gibt aber ein Iterable zurück, kein Array, und erzeugt seine Ergebnisse bei Bedarf.
function* mapIter(iterable, func) {
let index = 0;
for (const x of iterable) {
yield func(x, index);
index++;
}
}
const iterable = mapIter(['a', 'b'], x => x + x);
assert.deepEqual(
Array.from(iterable), ['aa', 'bb']
); Übung: Iterables filtern
exercises/sync-generators/filter_iter_gen_test.mjs
yield* aufrufenyield funktioniert nur direkt innerhalb von Generatoren – bisher haben wir keine Möglichkeit gesehen, das Yielding an eine andere Funktion oder Methode zu delegieren.
Betrachten wir zunächst, was nicht funktioniert: Im folgenden Beispiel möchten wir, dass foo() bar() aufruft, damit letzteres zwei Werte für ersteres yielded. Leider schlägt ein naiver Ansatz fehl.
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
// Nothing happens if we call `bar()`:
bar();
}
assert.deepEqual(
Array.from(foo()), []
);Warum funktioniert das nicht? Der Funktionsaufruf bar() gibt ein Iterable zurück, das wir ignorieren.
Was wir wollen, ist, dass foo() alles yielded, was bar() yielded. Das ist es, was der yield*-Operator tut.
function* bar() {
yield 'a';
yield 'b';
}
function* foo() {
yield* bar();
}
assert.deepEqual(
Array.from(foo()), ['a', 'b']
);Mit anderen Worten, das vorherige foo() ist ungefähr gleichbedeutend mit:
function* foo() {
for (const x of bar()) {
yield x;
}
}Beachten Sie, dass yield* mit jedem Iterable funktioniert.
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
Array.from(gen()), [1, 2]
);yield* ermöglicht uns rekursive Aufrufe in Generatoren, was beim Iterieren über rekursive Datenstrukturen wie Bäume nützlich ist. Nehmen Sie zum Beispiel die folgende Datenstruktur für binäre Bäume.
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
/** Prefix iteration: parent before children */
* [Symbol.iterator]() {
yield this.value;
if (this.left) {
// Same as yield* this.left[Symbol.iterator]()
yield* this.left;
}
if (this.right) {
yield* this.right;
}
}
}Die Methode [Symbol.iterator]() fügt Unterstützung für das Iterationsprotokoll hinzu, was bedeutet, dass wir eine for-of-Schleife verwenden können, um über eine Instanz von BinaryTree zu iterieren.
const tree = new BinaryTree('a',
new BinaryTree('b',
new BinaryTree('c'),
new BinaryTree('d')),
new BinaryTree('e'));
for (const x of tree) {
console.log(x);
}
// Output:
// 'a'
// 'b'
// 'c'
// 'd'
// 'e' Übung: Über verschachtelte Arrays iterieren
exercises/sync-generators/iter_nested_arrays_test.mjs
Als Vorbereitung auf den nächsten Abschnitt müssen wir zwei verschiedene Stile des Iterierens über die Werte „innerhalb“ eines Objekts lernen.
Externe Iteration (Pull): Ihr Code fragt das Objekt über ein Iterationsprotokoll nach den Werten. Zum Beispiel basiert die for-of-Schleife auf dem Iterationsprotokoll von JavaScript.
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'Interne Iteration (Push): Wir übergeben eine Callback-Funktion an eine Methode des Objekts, und die Methode liefert die Werte an den Callback. Zum Beispiel haben Arrays die Methode .forEach().
['a', 'b'].forEach((x) => {
console.log(x);
});
// Output:
// 'a'
// 'b'Der nächste Abschnitt enthält Beispiele für beide Iterationsstile.
Ein wichtiger Anwendungsfall für Generatoren ist das Extrahieren und Wiederverwenden von Traversierungen.
Betrachten wir als Beispiel die folgende Funktion, die einen Dateibaum durchläuft und seine Pfade protokolliert (sie verwendet die Node.js API dafür).
function logPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
console.log(filePath);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
logPaths(filePath); // recursive call
}
}
}Betrachten Sie das folgende Verzeichnis.
mydir/
a.txt
b.txt
subdir/
c.txt
Protokollieren wir die Pfade innerhalb von mydir/.
logPaths('mydir');
// Output:
// 'mydir/a.txt'
// 'mydir/b.txt'
// 'mydir/subdir'
// 'mydir/subdir/c.txt'Wie können wir diese Traversierung wiederverwenden und etwas anderes tun, als die Pfade zu protokollieren?
Eine Möglichkeit, Traversierungscode wiederzuverwenden, ist über interne Iteration: Jeder traversierte Wert wird an einen Callback übergeben (Zeile A).
function visitPaths(dir, callback) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
callback(filePath); // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
visitPaths(filePath, callback);
}
}
}
const paths = [];
visitPaths('mydir', p => paths.push(p));
assert.deepEqual(
paths,
[
'mydir/a.txt',
'mydir/b.txt',
'mydir/subdir',
'mydir/subdir/c.txt',
]);Eine andere Möglichkeit, Traversierungscode wiederzuverwenden, ist über externe Iteration: Wir können einen Generator schreiben, der alle traversierten Werte yieldet (Zeile A).
function* iterPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
yield filePath; // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* iterPaths(filePath);
}
}
}
const paths = Array.from(iterPaths('mydir'));Das Kapitel über Generatoren in Exploring ES6 behandelt zwei Themen, die über den Rahmen dieses Buches hinausgehen.
yield kann auch Daten empfangen, über ein Argument von .next().return (nicht nur yieldn). Solche Werte werden nicht zu Iterationswerten, können aber über yield* abgerufen werden.