原文: http://exploringjs.com/impatient-js/ch_sync-generators.html
同步生成器是函数定义和方法定义的特殊版本,它们始终返回同步可迭代:
// 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() {
// ···
}
}
星号(*
)将函数和方法标记为生成器:
function*
是关键字function
和星号的组合。*
是一个修饰符(类似于static
和get
)。yield
填充它们如果调用生成器函数,它将返回一个 iterable(实际上:也是可迭代的迭代器)。生成器通过yield
运算符填充可迭代的:
function* genFunc1() {
yield 'a';
yield 'b';
}
const iterable = genFunc1();
// Convert the iterable to an Array, to check what’s inside:
assert.deepEqual([...iterable], ['a', 'b']);
// You can also use a for-of loop
for (const x of genFunc1()) {
console.log(x);
}
// Output:
// 'a'
// 'b'
yield
暂停生成器功能到目前为止,yield
看起来像是一种向迭代中添加值的简单方法。但是,它做的远不止于此 - 它还会暂停和退出生成器功能:
return
类似,yield
退出函数体。return
不同,如果再次调用该函数,则会在yield
之后直接执行。让我们通过以下生成器函数来检查它的含义。
let location = 0;
function* genFunc2() {
location = 1;
yield 'a';
location = 2;
yield 'b';
location = 3;
}
生成器函数的结果称为 _ 生成器对象 _。它不仅仅是一个可迭代的,但这超出了本书的范围(如果您对更多细节感兴趣,请参考“探索 ES6”)。
为了使用genFunc2()
,我们必须首先创建生成器对象genObj
。 genFunc2()
现在暂停“身体前”。
const genObj = genFunc2();
// genFunc2() is now paused “before” its body:
assert.equal(location, 0);
genObj
实现迭代协议。因此,我们通过genObj.next()
控制genFunc2()
的执行。调用该方法,恢复暂停的genFunc2()
并执行它直到有yield
。然后执行暂停,.next()
返回yield
的操作数:
assert.deepEqual(
genObj.next(), {value: 'a', done: false});
// genFunc2() is now paused directly after the first `yield`:
assert.equal(location, 1);
请注意,产生的值'a'
包装在一个对象中,这就是迭代总是传递它们的值的方式。
我们再次调用genObj.next()
并继续执行我们先前暂停的位置。一旦遇到第二个yield
,genFunc2()
暂停,.next()
返回产生的值'b'
。
assert.deepEqual(
genObj.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the second `yield`:
assert.equal(location, 2);
我们再一次调用genObj.next()
并继续执行直到它离开genFunc2()
的主体:
genObj.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);
这次,.next()
的结果的属性.done
是true
,这意味着可迭代完成。
yield
暂停执行?yield
暂停执行有什么好处?为什么它不像 Array 方法.push()
那样工作并用值填充 iterable - 没有暂停?
由于暂停,生成器提供 _ 协同程序 _ 的许多功能(认为协同多任务的进程)。例如,当你要求迭代的下一个值时,该值被计算 _ 懒惰 _(按需)。以下两个生成器函数演示了这意味着什么。
/**
* 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++;
}
}
请注意,numberLines()
内的yield
出现在for-of
循环内。 yield
可以在循环内部使用,但不能在回调内部使用(稍后会详细介绍)。
让我们结合两个生成器来生成可迭代的numberedLines
:
const numberedLines = numberLines(genLines());
assert.deepEqual(
numberedLines.next(), {value: '1: A line', done: false});
assert.deepEqual(
numberedLines.next(), {value: '2: Another line', done: false});
每次我们通过.next()
向numberedLines
询问另一个值时,numberLines()
只询问genLines()
一行并给它编号。如果genLines()
同步从大文件中读取它的行,我们将能够从文件中读取第一个编号行。如果yield
没有暂停,我们必须等到genLines()
完全读完。
exercises/generators/fib_seq_test.js
以下函数mapIter()
类似于Array.from()
,但它返回一个可迭代的,而不是一个数组,并根据需要生成其结果。
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([...iterable], ['aa', 'bb']);
exercises/generators/filter_iter_gen_test.js
yield*
调用生成器yield
只能直接在生成器中工作 - 到目前为止,我们还没有看到将屈服委托给另一个函数或方法的方法。
让我们首先检查 _ 不是 _ 的工作原理:在下面的例子中,我们希望foo()
调用bar()
,以便后者为前者产生两个值。唉,天真的方法失败了:
function* foo() {
// Nothing happens if we call `bar()`:
bar();
}
function* bar() {
yield 'a';
yield 'b';
}
assert.deepEqual(
[...foo()], []);
为什么这不起作用?函数调用bar()
返回一个我们忽略的 iterable。
我们想要的是foo()
产生bar()
产生的所有东西。这就是yield*
运算符的作用:
function* foo() {
yield* bar();
}
function* bar() {
yield 'a';
yield 'b';
}
assert.deepEqual(
[...foo()], ['a', 'b']);
换句话说,之前的foo()
大致相当于:
function* foo() {
for (const x of bar()) {
yield x;
}
}
请注意,yield*
适用于任何可迭代:
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
[...gen()], [1, 2]);
yield*
允许我们在生成器中进行递归调用,这在迭代递归数据结构(如树)时很有用。举例来说,二叉树的数据结构如下。
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;
}
}
}
方法[Symbol.iterator]()
增加了对迭代协议的支持,这意味着我们可以使用for-of
循环迭代BinaryTree
的实例:
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'
exercises/generators/iter_nested_arrays_test.js
生成器的一个重要用例是提取和重用循环功能。
作为一个例子,考虑以下函数迭代文件树并记录它们的路径(它使用 Node.js API 这样做):
function logFiles(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()) {
logFiles(filePath); // recursive call
}
}
}
const rootDir = process.argv[2];
logFiles(rootDir);
我们如何重用这个循环来做除记录路径之外的其他事情?
重用迭代代码的一种方法是通过 _ 内部迭代 _ :将每个迭代值传递给回调(A 行)。
function iterFiles(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()) {
iterFiles(filePath, callback);
}
}
}
const rootDir = process.argv[2];
const paths = [];
iterFiles(rootDir, p => paths.push(p));
另一种重用迭代代码的方法是通过 _ 外部迭代 _ :我们可以编写一个生成所有迭代值的生成器。
function* iterFiles(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* iterFiles(filePath);
}
}
}
const rootDir = process.argv[2];
const paths = [...iterFiles(rootDir)];