21.3 用作迭代器的生成器(数据生产)
在学习本节之前,你应该对 ES6 迭代熟悉,上一章讲解了更多关于迭代的知识。
正如之前讲解的,生成器对象可以是数据生产者,数据消费者或者两者都是。本节将其视为数据生产者,同时实现 Iterable
和 Iterator
接口(如下所示)。这意味着一个生成器的结果既是一个 Iterable 对象又是一个 Iterator 对象。完整的生成器对象接口将会在后面展示。
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return?(value? : any) : IteratorResult;
}
interface IteratorResult {
value : any;
done : boolean;
}
生成器函数通过 yield
生成一组值,数据消费者通过继承自 Iterator 的方法 next()
来消费这些值。例如,下面的生成器函数生成值 'a'
和 'b'
。
function* genFunc() {
yield 'a';
yield 'b';
}
下面的交互展示了如何通过生成器对象 genObj
得到 yield 返回的值:
> let genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }
> genObj.next()
{ value: 'b', done: false }
> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }
21.3.1 迭代一个生成器的方式
由于生成器对象是可迭代的,所以 ES6 中支持迭代的语言结构都可以使用生成器对象。下面三种结构尤其重要。
首先是 for-of
循环:
for (let x of genFunc()) {
console.log(x);
}
// Output:
// a
// b
其次是扩展操作符(...),将可迭代的序列转换成一个数组的元素(参考关于参数处理的那一章来获取有关这个操作符的更多信息):
let arr = [...genFunc()]; // ['a', 'b']
第三个,解构:
> let [x, y] = genFunc();
> x
'a'
> y
'b'
21.3.2 从生成器返回
前面的生成器函数没有包含一个显示的 return
。隐式的 return
等价于返回 undefined
。让我们看看带有显示 return
的生成器:
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'result';
}
next()
返回的最后一个对象包含了返回值,并且对象的属性 done
为 true
:
> let genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }
但是,大多数做迭代的语法结构都忽略 done
属性里面的值:
for (let x of genFuncWithReturn()) {
console.log(x);
}
// Output:
// a
// b
let arr = [...genFuncWithReturn()]; // ['a', 'b']
yield*
是一个执行生成器嵌套调用的操作符,它会考虑 done
属性里面的值,这在后面讲解。
21.3.3 示例:迭代属性
让我们看一个示例,该例子展示了生成器实现迭代功能是多么的方便。下面的函数, objectEntries()
,返回一个对象属性的迭代器:
function* objectEntries(obj) {
// In ES6, you can use strings or symbols as property keys,
// Reflect.ownKeys() retrieves both
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
该函数使你能够通过 for-of
循环迭代对象 jane
的属性:
let jane = { first: 'Jane', last: 'Doe' };
for (let [key,value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
为了作为对比 - 一个不使用生成器的 objectEntries()
实现要复杂很多:
function objectEntries(obj) {
let index = 0;
let propKeys = Reflect.ownKeys(obj);
return {
[Symbol.iterator]() {
return this;
},
next() {
if (index < propKeys.length) {
let key = propKeys[index];
index++;
return { value: [key, obj[key]] };
} else {
return { done: true };
}
}
};
}
21.3.4 只能在生成器中使用 yield
一个值得注意的生成器的限制是只能在生成器函数中使用 yield 。也就是说,在回调函数中使用 yield 是没有效果的:
function* genFunc() {
['a', 'b'].forEach(x => yield x); // SyntaxError
}
不能在非生成器函数中使用 yield
,这就是为什么前面的代码引起了语法错误。在此种场景下,把代码改成不使用回调函数的形式是很容易的(如下所示)。但是不幸的是这不是总会奏效的。
function* genFunc() {
for (let x of ['a', 'b']) {
yield x; // OK
}
}
上面的限制在后面讲解:它们使生成器更容易实现,并且兼容事件循环。
21.3.5 通过 yield*
嵌套(用于输出)
只能在生成器函数中使用 yield
。因此,如果想实现一个生成器的嵌套逻辑,需要一种从其它生成器中调用某一个生成器的方式。本节会展示这种方式,实际上这比听起来的要复杂,这就是为什么 ES6 有一个特殊的操作符 yield*
。现在,我只解释在两个生成器都有输出的时候 yield*
是如何工作的,我会在后面讲解如果包含输入的话优势如何工作的。
一个生成器是如何嵌套调用另一个生成器的?假设已经有一个生成器函数 foo
:
function* foo() {
yield 'a';
yield 'b';
}
如何在另一个生成器函数 bar
中调用 foo
?下面的方式是不顶用的!
function* bar() {
yield 'x';
foo(); // does nothing!
yield 'y';
}
调用 foo()
返回一个对象,但是实际上并不会执行 foo()
。这就是为什么 ECMAScript 6 有操作符 yield*
,该操作符用于生成器的嵌套调用:
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// Collect all values yielded by bar() in an array
let arr = [...bar()];
// ['x', 'a', 'b', 'y']
在内部, yield*
类似于:
function* bar() {
yield 'x';
for (let value of foo()) {
yield value;
}
yield 'y';
}
yield*
的操作数不一定是生成器对象,也可以是可迭代的对象:
function* bla() {
yield 'sequence';
yield* ['of', 'yielded'];
yield 'values';
}
let arr = [...bla()];
// ['sequence', 'of', 'yielded', 'values']
21.3.5.1 用 yield*
拿到最后一个值
大多数支持迭代的语法结构都会忽略迭代的最后一个对象( done
属性为 true
)。生成器通过 return
返回最后一个值。 yield*
表达式的值就是迭代的最后一个对象:
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result); // (A)
}
如果想执行到行 A ,首先就要迭代掉 logReturned()
中生成的所有值:
> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]
21.3.5.2 遍历树
用递归的方式遍历一棵树是很简单的,用传统的方式给一颗树添加一个迭代器是很复杂的。这就是为什么生成器在此处大放异彩:可以通过递归实现一个迭代器。作为一个例子,考虑下面的二叉树的数据结构。它是可迭代的,因为有一个键为 Symbol.iterator
的方法,该方法是一个迭代器方法,在调用的时候返回一个迭代器。
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
/** Prefix iteration */
* [Symbol.iterator]() {
yield this.value;
if (this.left) {
yield* this.left;
}
if (this.right) {
yield* this.right;
}
}
}
下面的代码创建了一颗二叉树,并通过 for-of
循环遍历:
let tree = new BinaryTree('a',
new BinaryTree('b',
new BinaryTree('c'),
new BinaryTree('d')),
new BinaryTree('e'));
for (let x of tree) {
console.log(x);
}
// Output:
// a
// b
// c
// d
// e