迭代器和生成器 | 生成器(含手写生成器应用)
生成器
生成器是ES6新增的解构,拥有在一个函数块内暂停和恢复代码执行的能力
生成器基础
生成器的形式是一个函数,函数名称前面加一个星号(*)【星号不受两侧空格影响】表示它是一个生成器
1 | /* |
调用生成器函数会生成一个生成器对象
- 生成器对象一开始处于暂停状态(
suspended
)
- 生成器对象一开始处于暂停状态(
生成器方法也实现了Iterator接口,因此也可以调用
next()
方法,一旦调用这个方法,就会让生成器开始或者恢复执行(相当于激活了~)next()
方法返回值类似于迭代器,有一个done
属性和一个value
属性函数体为空的生成器中间不会停留,调用一次
next()
就会让生成器达到done:true
状态1
2
3
4
5
6function *generatorFn() {} // 定义一个生成器对象,生成器对象是一个函数
let g = generatorFn(); // 调用生成器对象
console.log(g); // generatorFn {<suspended>} 生成器对象处于暂停状态
console.log(g.next()); // {value: undefined, done: true}value
属性是生成器的返回值,默认为undefined
,可以通过生成器函数的返回值指定1
2
3
4
5
6
7
8function *generatorFn() {
return 'foo'
}
let g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next()); // {value: 'foo', done: true}
生成器函数只会在初次调用
next()
方法后开始执行1
2
3
4
5
6
7
8function *generatorFn() {
console.log('foo');
}
let g = generatorFn(); // 并不会打印foo
console.log(g); // generatorFn {<suspended>}
g.next() // 'foo'生成器对象实现了Iterator接口,它们默认的迭代器是自引用的
1
2
3
4
5
6
7
8
9function *generatorFn() {}
console.log(generatorFn); // ƒ *generatorFn() {}
console.log(generatorFn()[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(generatorFn()); // generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>}
const g = generatorFn();
console.log(g === g[Symbol.iterator]()); // true
通过yield中断执行
yield关键字可以让生成器停止和开始执行
yield关键字只能在生成器函数内部使用
yield可以作为函数的中间返回语句使用,yield关键字可以作为函数的中间参数使用,yield关键字可以同时用于输入和输出
- 生成器函数在遇到
yield
关键字之前正常执行,遇到这个关键字之后,执行会停止,函数作用域的状态会被保留,停止执行的生成器函数只能通过在生成器对象上调用next()
方法来恢复执行
1 | function *generatorFn() { |
- yield关键字有点像函数中间的返回语句,它生成的值会出现在
next()
方法返回的对象里(不同的是:通过yield关键字退出的生成器函数会处在done:false
状态;通过retrun
关键字退出的生成器函数会处于done:true
状态)
1 | function *generatorFn() { |
- 生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器
1 | function *generatorFn() { |
(1)生成器对象作为可迭代对象
1 | function *generatorFn() { |
应用场景:我们需要定义一个可迭代对象,而它会产生一个迭代器,这个迭代器会执行指定的次数。使用生成器,可以通过一个简单的循环来实现:
1 | function *nTimes(n) { // 这个函数可以控制迭代循环的次数 |
(2)使用yield实现输入和输出
注意: 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
1 | // yield关键字可以作为函数的中间参数使用 |
因为函数必须对整个表达式求值才能确定要返回的值,所以它在遇到yield
关键字时暂停执行并计算出要产生的值:"foo"
,下一次调用next()
传入了"bar"
,作为交给同一个yield
的值,然后这个值被确定为本次生成器函数要返回的值
(3)yield关键字可以允许使用多次
应用场景1:定义一个生成器函数,它会根据配置的值迭代相应次数并产生迭代的索引
1 | // 实现1 |
应用场景2:使用生成器实现范围
1 | function *range(start, end) { |
应用场景3:使用生成器实现填充数组
1 | function *zeros(n) { |
(4)产生可迭代对象
可以使用星号(*)【星号两侧的空格不影响其行为】增强yield的值,让它能够迭代一个可迭代对象,从而一次产出一个值
yield*
实际上只是将一个可迭代对象序列化为一连串可以单独产出的值
1 | // 等价的generatorFn |
yield*
的值是关联迭代器返回done:true
时的value
属性- 对于普通的迭代器来说,这个值是
undefined
1
2
3
4
5
6
7
8
9
10
11
12function * generatorFn() {
console.log('iter value:', yield*[1,2,3])
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
// iter value: undefined- 对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function *innerGeneratorFn() {
yield 'foo';
return 'bar';
}
function *outerGeneratorFn(genObj) {
console.log('iter value:', yield * innerGeneratorFn())
}
for (const x of outerGeneratorFn()) {
console.log('value:', x);
}
// value: foo
// iter value: bar- 对于普通的迭代器来说,这个值是
(5)使用yield*实现递归算法
实现递归操作是yield*最有用的地方
1 | function * nTimes(n) { |
应用场景:使用生成器测试一个随机的双向图是否联通
1 | // 创建节点 节点的邻里关系 |
生成器函数必须接收一个可迭代对象,产出该对象中的每一个值,并且对每个值进行递归,这个实现可以用来测试某个图是否连通,即是否没有不可到达的节点。只要从一个节点开始,然后尽力访问每个节点就可以了;结果就得到了一个非常简洁的深度优先遍历:
1 | // 创建节点 节点的邻里关系 |
生成器作为默认迭代器
因为生成器实现了Iterator接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器适合作为默认迭代器
1 | class Foo { |
提前终止生成器
return() 和 throw() 方法都可以用于强制生成器进入关闭状态
(1)return()
return()方法会强制生成器进入关闭状态,提供给return的值就是终止迭代器对象的值
1 | function * generatorFn() { |
与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了
后续调用next()会显示done: true 状态,而提供的任何返回值都不会被存储或传播
1 | function * generatorFn() { |
for-of
循环等内置语言结构会忽略状态为done: true
的IteratorObject
内部返回的值
1 | function * generatorFn() { |
(2)throw()
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被处理,生成器就会关闭
1 | function * generatorFn() { |
- 假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行,错误处理会跳过对应的
yield
,因此在这个例子中会跳过一个值
1 | function * generatorFn() { |
在这个例子中,生成器在try/catch
块中的yield
关键字处暂停执行。在暂停期间,throw()
方法向生成器对象内部注入了一个错误:字符串"foo"
。这个错误会被yield
关键字抛出。因为错误是在生成器的try/catch
块中抛出的,所以仍然在生成器内部被捕获可是,由于yield
抛出了那个错误,生成器就不会再产出值2
。此时,生成器函数继续执行,在下一次迭代再次遇到yield
关键字时产出了值3
注意:如果生成器对象还没有开始执行,那么调用
throw()
抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误
1 | function * generatorFn() { |