生成器是ES6新增的一个灵活结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程。
生成器基础
- 生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要可以定义函数的地方就可以定义生成器。
// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function * () {}
// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
static * generatorFn() {}
}
-
注意:箭头函数不能用来定义生成器函数
-
标识生成器函数的星号不受两侧空格的影响
-
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行suspended的状态。
-
与迭代器一致,生成器对象也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行。
function * generatorFn () {}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() { [native code]}
-
next()方法的返回值类似迭代器,{done: false, value: }
- value属性是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定:
function * generatorFn() { return 'foo'; } let generatorObject = generatorFn(); generatorObject.next(); // {done: true, value: 'foo'}
-
生成器函数只会在初次调用next()方法后执行:
function * generatorFn() {
console.log('foobar')
}
// 初次调用生成器函数并不会打印日志
let generatorObject = generatorFn();
// 只有调用next()方法之后才会打印日志
generatorObject.next(); // foobar
- 生成器对象实现了Iterable接口,它们漠然的迭代器是自引用的:
function * generatorFn() {}
console.log(generatorFn); // f* generatorFn
console.log(generatorFn()[Symbol.iterator]);
// f [Symbol.iterator]() {native code}
console.log(generatorFn());
// generatorFn {<suspended>}
console.log(generatorFn()[symbol.iterator]());
// generatorFn {<suspended>}
const g = generatorFn();
g === g[Symbol.iterator]() // true
通过yield中断执行
-
yield关键字可以让生成器停止和开始执行,也是生成器最有用的地方。
- 生成器函数在遇到yield关键字之前会正常执行。
- 遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。
function * generatorFn() { yield; } let generatorObject = generatorFn(); generatorObject.next(); // {done: false, value: undefined} generatorObject.next(); // {done: true, value: undefined}
- 此时的yield关键字有点像函数的中间返回语句,它生成的值会出现在next()方法返回的对象里。
- 通过yield关键字退出的生成器函数会处在done: false状态;
- 通过return关键字退出的生成器函数会处于done: true状态.
function * generatorFn() { yield 'foo'; yield 'bar'; return 'baz'; } let generatorObejct = generatorFn(); generatorObject.next(); // { done: false, value: 'foo'}; console.log(generatorObject.next()); // { done: false, value: 'bar' } console.log(generatorObject.next()); // { done: true, value: 'baz' }
- 生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用next()不会影响其他生成器:
function * generatorFn () { yield 'foo'; yield 'bar'; return 'baz'; } let genObj1 = generatorFn(); let genObj2 = generatorFn(); genObj1.next(); // {done: false, vlaue: 'foo'} genObj2.next(); // {done: false, vlaue: 'foo'}
- yield关键字只能在生成器函数内部使用,在其他地方使用会报错。
- yield关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。
-
生成器对象作为可迭代对象
- 在生成器对象上显式调用next()方法的用处并不大。其实,如果把生成器对象当成可迭代对象,那么使用起来会更方便:
function * genFn() { yield 1; yield 2; yield 3; } for (const x of genFn()) { console.log(x); } // 1 // 2 // 3
- 需要自定义迭代对象时,这样使用生成器对象特别有用。
-
使用yield实现输入和输出
- 除了可以作为函数的中间返回语句使用,yield关键字还可以作为函数的中间参数使用。
- 上次让生成器函数暂停的yield关键字会被接收到传给next()方法的第一个值。
- 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
function * generatorFn(initial) { console.log(initial); console.log(yield); console.log(yield); } let generatorObject = generatorFn('foo'); generatorObject.next('bar'); // foo generatorObject.next('baz'); // baz generatorObject.next('qux'); // qux
- yield关键字可以同时用于输入和输出
function * generatorFn() { return yield 'foo'; } let generatorObj = generatorFn(); generatorObj.next(); // {done: false, value: 'foo'} generatorObj.next('obj'); // {done: true, value: 'bar}
-
因为函数必须对整个表达式求值才能确定要返回的值,所以它在遇到yield关键字时暂停执行并计算出要产生的值:“foo”。下一次调用next()传入了"bar",作为交给同一个yield的值。然后这个值被确定为本次生成器函数要返回的值.
-
yield关键字并非只能使用一次。
-
产生可迭代对象
- 可以使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而产出一个值:
// 等价的generatorFn: function * generatorFn() { for (const i of [1,2,3]) { yield x; } } // 等价于上 function * generatorFn() { yield* [1,2,3] } let generatorObj = generatorFn(); for (const x of generatorObj) { console.log(x); } // 1 // 2 // 3
- yield 星号两侧空格不影响其行为。
- 因为yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的值,所以这跟把yield放到一个循环里没什么不同。
- yield*的值是关联迭代器返回done: true时的value属性。对于普通迭代器来说,这个值是undefined。
function * gen() { console.log('iter value:', yield* [1,2,3]); } for(const x of gennn()) { console.log('value: ', x); } // value: 1 // value: 2 // value: 3 // iter value: undefined
- 对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值:
function * innerGen() { yield 'foo'; return 'bar'; } function * outerGen(genObj) { console.log('iter value: ', yield * innerGen()); // 所以 yield * innerGen()返回的值就是innerGen()函数返回的值'bar' } for (const x of outerGen()) { console.log('value: ', x); } // value: foo // iter value: bar // 因此遍历outerGen()会先执行yield 'foo',打印foo,然后打印bar
-
使用yield*实现递归算法
- yield*最有用的地方就是实现递归操作,此时生成器可以产生自身:
function * nTimes(n) { if (n > 0) { yield* nTime(n-1); yield n-1; } } for (const x of nTimes(3)) { console.log(x); } // 0 // 1 // 2
-
在这个例子中,每个生成器首先都会从新创建的生成器对象产出每个值,然后再产出一个整数。结果就是生成器函数会递归地减少计数器值,并实例化另一个生成器对象。从最顶层来看,这就相当于创建一个可迭代对象并返回递增的整数.
-
图的递归遍历,要看!!!
提前终止生成器
- 与迭代器类似,生成器也支持“可关闭”的概念。一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。
function * generatorFn() {
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() {native code}
console.log(g.return); // f return() {native code}
console.log(g.throw); // f throw() {native code}
}
- return()和throw()方法可以用于强制生成器进入关闭状态。
-
return()
- return()方法会强制生成器进入关闭状态。提供给return()方法的值,就是终止迭代器对象的值.
- 与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。后续调用next()会显示done: true状态,而提供的任何返回值都不会被存储或传播.
- for-of循环等内置语言结构会忽略状态为done: true的IteratorObject内部返回的值。
-
throw()
-
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
-
不过,假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的yield,因此在这个例子中会跳过一个值。
-
如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。
-
【问】ES的错误捕获处理机制?