1、 什么是生成器
生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数也是一个函数,但是和普通的函数有一些区别:
首先,生成器函数需要在function的后面加一个符号:*
其次,生成器函数可以通过yield关键字来控制函数的执行流程
最后,生成器函数的返回值是一个Generator(生成器)
生成器事实上是一种特殊的迭代器
2、 生成器的声明
生成器是一个函数的形式,通过在函数名称前加一个星号(*)就表示它是一个生成器。所以只要是可以定义函数的地方,就可以定义生成器
function* gFn() { }
const gFn = function* () { }
const o = {
* gFn() { }
}
箭头函数不能用来定义生成器函数,因为生成器函数使用( function*)语法编写。
那为啥上面的第三个例子可以不使用 (function*)语法呢?
因为那个是简写版本。等价于:
const o = {
gFn: function* () { }
}
3、 生成器的使用
3.1 生成器函数执行
function* gFn() {
console.log(111)
}
gFn()
然后,我们会很"开心"地发现控制台没有打印任何信息。
这是因为调用生成器函数会产生一个生成器对象,但是这个生成器一开始处于暂停执行的状态,需要调用 next方法才能让生成器开始或恢复执行。
return会直接让生成器到达 done: true状态:
function* gFn() {
console.log(111)
return 222
}
const g = gFn()
console.log("1",g)
console.log("2",g.next())
console.log("3",g.next())
生成器代码的执行可以被yield控制:
function* gFn() {
yield
console.log(111)
return 222
}
const g = gFn()
console.log("1",g)
console.log("2",g.next())
console.log("3",g.next())
yield关键字
提到生成器,自然不能忘记 yield关键字。 yield能让生成器停止,此时函数作用域的状态会被保留,只能通过在生成器对象上调用 next方法来恢复执行。
上面我们已经说了, return会直接让生成器到达 done: true状态,而 yield则是让生成器到达 done: false状态,并停止
我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果:
function* gFn() {
yield 100
console.log(111)
return 222
}
const g = gFn()
console.log("1",g)
console.log("2",g.next())
console.log("3",g.next())
3.2 生成器传递参数
函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
答案是可以的, 我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;
function* gFn(initial) {
console.log(initial)
console.log(yield)
console.log(yield)
console.log(yield)
}
const g = gFn('red')
g.next('white')
g.next('blue')
g.next('purple')
- 生成生成器,此时处于暂停执行的状态
- 调用 next,让生成器开始执行,输出 red,然后准备输出 yield,发现是 yield,暂停执行,出去外面一下。
- 外面给 next方法传参 blue,又恢复执行,然后之前暂停的地方(即 yield)就会接收到 blue。然后又遇到 yield暂停。
- 又恢复执行,输出 purple
然后,可能就会有人问,第一次传的 white怎么消失了?
它确实消失了,因为第一次调用 next方法是为了开始执行生成器函数,而刚开始执行生成器函数时并没有 yield接收参数,所以第一次调用 next的值并不会被使用。
yield关键字同时用于输入和输出: yield可以和 return同时使用,同时用于输入和输出
function* gFn() {
yield 111
return yield 222
}
const g = gFn()
console.log(g.next(333))
console.log(g.next(444))
console.log(g.next(555))
- 生成生成器,此时处于暂停执行的状态
- 调用 next,让生成器开始执行,遇到 yield,暂停执行,因为 yield后面还有111,所以带着111作为输出出去外面。
- 调用 next,生成器恢复执行,遇到 return,准备带着后面的数据跑路,结果发现后面是
yield,所以又带着222,作为输出到外面。 - 调用 next,又又又恢复执行,不过这个时候return的内容是 yield表达式,所以 yield会作为输入接收555,然后再把它带到外面去输出。
yield表达式需要计算要产生的值,如果后面没有值,那就默认是 undefined。 return yield x的语法就是,遇到 yield,先计算出要产生的值 111,在暂停执行的时候作为输出带出去,然后调用 next方法时, yield又作为输入接收 next方法的第一个参数
3.3 生成器抛出异常
throw也可以提前终止生成器,且会抛出异常,需要捕获处理抛出的异常。
function* gFn() {
yield 111
yield 222
yield 333
}
const g = gFn()
// g.throw(444) // 如果异常没有被处理的话,会直接报错
try {
g.throw(444)
} catch (e) {
console.log(e)
}
console.log(g)
console.log(g.next())
不过,如果处理异常是在生成器内部的话,情况就不太一样了。
function* gFn() {
for (const x of [1, 2, 3]) {
try {
yield x
} catch (e) {
console.log(e)
}
}
}
const g = gFn()
console.log(g.next())
g.throw(444)
console.log(g)
console.log(g.next())
如果是在生成器内部处理这个错误,那么生成器不会关闭,还可以恢复执行,只是会跳过对应的yield,即会跳过一个值。
4、 生成器替代迭代器
我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
const names = ["aaa", "bbb", "ccc"]
function* gFn(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i]
}
}
const g = gFn(names )
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
事实上我们还可以使用yield*来生产一个可迭代对象:这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;
const names = ["aaa", "bbb", "ccc"]
function* gFn(arr) {
yield* arr
}
const g = gFn(names)
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
结果同上!