js 中的生成器函数与生成器
上篇文章我们说过迭代器中的 next()
方法一般不接受参数,但是有个例外,就是生成器。本文就来介绍下 js 中的生成器相关内容,虽然有些许枯燥,但理解它可以帮助我们更好的理解 async/await 的本质(后续文章细说)。
概念
生成器函数(Generator functions)是 ES6 提供的一种解决异步编程的方案。当我们在 function
与函数名之间加一个 *
后,这个函数就变为了生成器函数(注意,生成器函数不可以用箭头函数定义),至于 *
和 function
或函数名之间加不加或加几个空格都可以,但一般习惯于把 *
直接写在 function
后,然后空一格再写函数名:
// 例 1
function* myGenerator() {}
现在,myGenerator 就是一个生成器函数,调用它不会执行函数内部逻辑。而是返回一个叫做生成器(Generator )的特殊的迭代器:
// 例 1.1
const generator = myGenerator()
生成器的方法
next()
因为是迭代器,所以可以调用next()
方法,然后生成器函数内部的逻辑就会开始执行,直到遇到关键字 yield
停止。再次调用 next()
方法,会从上一次停止时的下一行开始执行。如此,就可以让我们灵活地控制函数什么时候暂停执行,什么时候继续执行:
// 例 1.2
function* myGenerator() {
console.log('第一次执行 next 方法')
yield
console.log('第二次执行 next 方法')
yield
console.log('第三次执行 next 方法')
}
const generator = myGenerator()
generator.next() // 第一次执行 next 方法
generator.next() // 第二次执行 next 方法
generator.next() // 第三次执行 next 方法
与一般的迭代器调用 next 方法的返回值一样,生成器调用 next 的返回值也是一个拥有 value
和 done
属性的对象,value
值为 yield
后的表达式结果,默认为 undefined
;done
的值代表生成器函数内的代码是否已经执行完毕,是则为 true
,否则为 false
:
// 例 1.3
function* myGenerator() {
console.log('1')
yield 'Jay'
console.log('2')
yield 'Zhou'
console.log('3')
yield 'Jay' + 'Zhou'
console.log('4')
}
const generator = myGenerator()
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
执行结果如下图:
第 4 次执行 next 方法,代码从第 9 行开始执行,之后就结束了,所以返回的结果中 done
为 true
;因为没有指定返回值,相当于 return undefined
,所以 value
为 undefined
。如果我们提前定义了返回值,比如在例 1.3 的第 2 个 yield
之前写上一句 return '返回'
:
// 例 1.3.1
function* myGenerator() {
console.log('1')
yield 'Jay'
console.log('2')
return '返回'
yield 'Zhou'
console.log('3')
yield 'Jay' + 'Zhou'
console.log('4')
}
则打印结果会变成下图所示:
也就是说遇到 return
后,函数就会结束执行,done
变为 true
,之后不管再执行多少次 generator.next()
,返回的对象都是 { value: undefined, done: true }
。
传参
之前介绍迭代器的时候说过,迭代器的 next()
方法是个无参数或只有 1 个参数的函数。在生成器这个特殊的迭代器中,就可以传入这里所说的 1 个参数,作为函数继续执行时上一个 yield
语句的返回值:
// 例 2
function* myGenerator() {
const num = yield 'Jay'
console.log(num) // 2
yield 'Zhou'
}
const generator = myGenerator()
generator.next()
generator.next(2)
generator.next()
比如例 2 中,第 8 行第 1 次调用了 next 方法,代码执行到第 3 行第 1 个 yield
暂停,然后第 9 行第 2 次调用 next,并且传入了参数 2
,该参数会作为第 3 行第 1 个 yield 语句的返回值接收,那么继续从第 4 行开始执行时就可以使用传入的参数了。
在第 1 个 yield
之前如果需要使用传入的参数,我们可以在调用生成器函数时传入:
// 例 2.1
function* myGenerator(num) {
console.log(num) // 1
yield 'Jay'
}
const generator = myGenerator(1)
generator.next()
return()
生成器还有个 return 方法:
// 例 3
function* myGenerator() {
console.log('第一段代码')
const num = yield 'Jay'
console.log('第二段代码', num)
yield 'Zhou'
}
const generator = myGenerator()
console.log(generator.next())
console.log(generator.return(2))
console.log(generator.next())
执行的结果如下图:
可以发现,第 5 行的打印并没有执行,说明当我们调用了生成器的 return 方法,就相当于在第 4 行执行了 return num
,直接将传入的参数返回,提前终止了生成器函数内部的代码的执行,之后再调用 next 方法,返回的均为 { value: undefined, done: true }
。
throw()
如下面的例 4,当我们在第 15 行,也就是调用了 1 次 next()
方法后,调用生成器的 throw()
方法,会导致第 5 行的第 1 个 yield 语句出现错误,如果我们不对其进行捕获,生成器函数内部代码执行到第 1 个 yield 时就终止。而如果我们使用 try catch
进行了异常捕获,则不会影响到之后调用 next()
让代码继续执行:
// 例 4
function* myGenerator() {
console.log(1)
try {
yield 'Jay'
} catch (error) {
console.log('error', error)
}
console.log(2)
yield 'Zhou'
console.log(3)
}
const generator = myGenerator()
console.log(generator.next())
console.log(generator.throw('我是异常'))
console.log(generator.next())
打印结果如下:
生成器替代迭代器
既然生成器也是迭代器,那么我们就可以在一些场景里用生成器去替代迭代器。下面举 2 个例子:
案例一
有如下生成迭代器的函数 makeIterator
:
// 例 5
function makeIterator(arr) {
let index = 0
return {
next() {
if (index < arr.length) {
return { value: arr[index++], done: false }
} else {
return { done: true }
}
}
}
}
// 例 5.1
const it = makeIterator([1, 2, 3])
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
执行例 5.1 得到的结果如下:
我们可以将 makeIterator
变为生成器函数:
// 例 5.2
function* makeIterator(arr) {
for (const item of arr) {
yield item
}
}
如此,每次执行 it.next()
返回的对象的 value
值就是 yield
后面的 item
,执行例 5.1 代码的结果如下,与原先例 5 一致,但是 makeIterator
的代码简洁了许多:
yield*
对于例 5.2 的 makeIterator
,还可以再用 yield* 表达式简化:
// 例 5.3
function* makeIterator(arr) {
yield* arr
}
yield*
后面跟上可迭代对象,依次迭代该对象,每次迭代其中 1 个值,相当于例 5.2 这种写法的语法糖。yield*
后面也可以是另一个生成器
案例二
之前在《迭代器与可迭代对象》中,例 2.2.3 写了个实例为可迭代对象的类 IterableClass
,该类的实例方法 [Symbol.iterator]
也可以改写成生成器函数:
// 例 5.4
class IterableClass {
constructor(arr) {
this.arr = arr
}
*[Symbol.iterator]() {
yield* this.arr
}
}
注意,在类的声明中,如果要把某个方法变为生成器函数, *
直接写在方法名的前面。
转载自:https://juejin.cn/post/7240081817722535995