生成器

生成器可以在函数内暂停和恢复执行。

生成器基础

生成器的形式是函数,在函数名前面加“*”,表示它是一个生成器。所有可以定义函数的地方都可以定义生成器,除了箭头函数。
标识生成器函数的星号不受空格限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成器函数声明
function* Fun() {}
// 生成器函数表达式
let fun = function* (){}
// 对象字面量
let foo = {
* fun(){}
}
// 作为实例方法的生成器函数
class Foo {
* fun(){}
}
// 作为静态方法的生成器函数
class Foo {
static * fun(){}
}

调用生成器函数会产生一个生成器对象。生成器对象一开始是暂停(Suspend)状态。与迭代器相似,生成器也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行。

1
2
3
4
5
6
function * generatorFn() {}
let g = generatorFn()
console.log(g)
// generatorFn {<suspended>}
g.next()
// {value: undefined, done: true}

next()方法的返回值类似迭代器,有一个done属性和value属性。函数体为空的生成器函数中间不会停留。调用next()就会让生成器到达done: true状态。

value属性是生成器函数的返回值,默认undefiend,可以通过生成器函数的返回值指定:

1
2
3
4
5
6
function * generatorFn() {
return 'foo'
}
let g = generatorFn()
console.log(g) // generatorFn {<suspended>}
console.log(g.next()) // {value: 'foo', done: true}

生成器实现了Iterator接口,它们默认的迭代器是自引用的。

1
g === g[Symbol.iterator]() // true

通过yield中断执行

yield关键字可以让生成器停止和开始执行。

生成器函数在遇到yield关键字之前正常执行,遇到这个关键字后,停止执行,函数作用域的状态会被保留,停止执行的生成器函数只能调用生成器对象的next()方法来恢复执行。

1
2
3
4
5
6
7
8
9
10
11
function * generatorFn() {
yield
}
let g = generatorFn()
// generatorFn {<suspended>}
g.next()
// {value: undefined, done: false}
g.next()
// {value: undefined, done: true}
g.next()
// {value: undefined, done: true}

yield有点像函数的中间返回语句,它生成的值会作为next()方法的返回对象。通过yield关键字退出的生成器函数处于done: false状态,通过return退出的生成器函数会处于done: true状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function * generatorFn() {
yield 'foo'
yield 'bar'
return 'zoo'
}
let g = generatorFn()
g.next()
{value: 'foo', done: false}
g.next()
// {value: 'bar', done: false}
g.next()
// {value: 'zoo', done: true}
g.next()
// {value: undefined, done: true}

生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器:

1
2
3
4
5
6
7
8
9
function * generatorFn() {
yield 'foo'
}
let g1 = generatorFn()
let g2 = generatorFn()
g1.next()
// {value: 'foo', done: false}
g2.next()
// {value: 'foo', done: false}

yield关键字只能在生成器函数内部使用,用在其他地方会抛出错误。
yield关键字必须直接位于生成器函数定义中,出现在嵌套的生成器函数会抛出错误。

1.生成器函数作为可迭代对象

显式调用生成器对象的next()方法的作用不大,把生成器对象当成可迭代对象会更加方便:

1
2
3
4
5
6
7
8
9
10
11
function * generatorFn() {
yield 1
yield 2
yield 3
}
for (let i of generatorFn()) {
console.log(i)
}
// 1
// 2
// 3

2.使用yield实现输入输出

第一次调用next()方法传入的值不会被使用,因为这一次调用为了开始执行生成器函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function * generatorFn(initial) {
console.log(initial)
console.log(yield)
console.log(yield)
}
let g = generatorFn('1')
g.next('2')
// 1
// {value: undefined, done: false}
g.next('3')
// 3
// {value: undefined, done: false}
g.next('4')
// 4
// {value: undefined, done: true}

yield关键字可同时用于输入和输出:

1
2
3
4
5
6
7
8
9
10
11
function * generatorFn() {
return yield 'foo'
}
let g = generatorFn()

g.next()
// {value: 'foo', done: false}
g.next('bar')
// {value: 'bar', done: true}
g.next('bar1')
// {value: undefined, done: true}

3.产生可迭代对象

可以使用星号增强yield的行为,让它能够迭代一个可迭代对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function * generatorFn() {
for (let i of [1,2,3]) {
yield i
}
}
let g = generatorFn()
for (let i of g) {
console.log(i)
}
// 1
// 2
// 3

function * generatorFn1() {
yield * [1,2,3]
}
let g1 = generatorFn1()
for (let i of g1) {
console.log(i)
}
// 1
// 2
// 3

与生成器函数类似,yield关键字星号不受两侧空格影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function * generatorFn1() {
yield* [1,2,3]
yield *[4,5]
yield * [6,7]
}
let g1 = generatorFn1()
for (let i of g1) {
console.log(i)
}
// 1
// 2
// 3
// 4
// 5
// 6
// 7

4.使用yield实现递归

1
2
3
4
5
6
7
8
9
10
11
12
function * nTimes(n) {
if (n > 0) {
yield * nTimes(n - 1)
yield n - 1
}
}
for (let n of nTimes(3)) {
console.log(n)
}
// 0
// 1
// 2

生成器作为默认迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo {
constructor() {
this.values = [1, 2, 3]
}
* [Symbol.iterator]() {
yield * this.values
}
}
const f = new Foo()
for (let x of f) {
console.log(x)
}
// 1
// 2
// 3

提前终止生成器

一个实现Iterator接口的对象一定有next()方法和一个可选的return()方法用于提前终止迭代器。
生成器对象除了有next,return方法还有throw()方法。

1
2
3
4
5
6
7
8
9
10
11
function * generatorFn() {}
let g = generatorFn()
console.log(g)
console.log(g.next)
console.log(g.return)
console.log(g.throw)

// generatorFn {<suspended>}
// ƒ next() { [native code] }
// ƒ return() { [native code] }
// ƒ throw() { [native code] }

return和throw都可以强制生成器进入关闭状态。

1.return()

提供给return的值就是终止迭代器对象的值:

1
2
3
4
5
6
7
8
9
10
function * generatorFn() {
yield * [1,2,3]
}
let g = generatorFn()
console.log(g)
// generatorFn {<suspended>}
console.log(g.return(4))
// {value: 4, done: true}
console.log(g)
// generatorFn {<closed>}

与迭代器不同,所有生成器对象都有return方法,只要通过它进入关闭状态就无法恢复了,后续调用next()会显示done: true状态,而提供的任何值都不会被存储。

1
2
3
4
5
6
7
8
9
10
11
12
function * generatorFn() {
yield * [1,2,3]
}
let g = generatorFn()
console.log(g)
// generatorFn {<suspended>}
console.log(g.return(4))
// {value: 4, done: true}
console.log(g)
// generatorFn {<closed>}
console.log(g.next(5))
// {value: undefined, done: true}

2.throw()

throw方法会在暂停的时候提供一个错误注入到生成器对象中,如果错误未被处理,生成器就会关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function * generatorFn() {
yield * [1,2,3]
}
let g = generatorFn()
console.log(g)
try {
g.throw('foo')
} catch (e) {
console.log('catch:', e)
}
console.log(g)
// generatorFn {<suspended>}
// catch: foo
// generatorFn {<closed>}

如果在生成器函数内部处理了这个错误,生成器就不会被关闭,而且还可以恢复执行,错误处理会跳过yield,因此在这个例子中会跳过一个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function * generatorFn() {
for (let i of [1,2,3]) {
try {
yield i
} catch (error) {
console.log('catch:', error)
}
}

}
let g = generatorFn()
console.log(g)
// generatorFn {<suspended>}
g.next()
{/* {value: 1, done: false} */}
g.throw('foo')
{/* catch: foo */}
g.next()
{/* {value: 3, done: false} */}

如果生成器函数还没有开始执行,那么调用throw抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。

小结

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器 工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。

迭代器必须通过连续调用 next()方法才能连续取得值,这个方法返回一个 IteratorObject。这 个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后 者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next()方法来消费,也可以通过原生消 费者,比如 for-of 循环来自动消费。

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口, 因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够 暂停执行生成器函数。使用 yield 关键字还可以通过 next()方法接收输入和产生输出。在加上星号之 后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。