1,什么是生成器
生成器在行为上与迭代器类似,但是二者并不能混为一谈。如下就是一个最简单的生成(generator function)
function* genratorFn() {
yield 1
yield 2
yield 3
}
你可以用以下任意方式声明一个生成器:
function* generator() {}
const generator = function* () {}
const my = {
*generator() {},
}
注意:不能用箭头函数的形式声明一个生成器函数。
2,什么是生成器对象
调用生成器,会生成一个待执行状态(<suspended>)的生成器对象(Generator)。该对象为一个可迭代对象,符合可迭代协议,实现迭代接口。并且,其调用其迭代器接口返回的就是生成器对象本身。
const generator = genratorFn()
console.log(generator) //genrator?{<suspended>}
console.log(generator[Symbol.iterator]() === generator)//true
待执行状态的生成器对象的原型对象,指向的是生成器对象,而生成器对象,是GeneratorFunction构造函数的实例(该构造函数并不是一个全局对象,所以你无法直接获取或使用。但是可以通过实例上的constructor属性获取)。如下所示:
const generator = genratorFn()
console.log(generator.next())//{value: 1, done: false}
const generatorProto = Object.getPrototypeOf(generator)
console.log(generatorProto) //GeneratorFunction的实例
const originProto = Object.getPrototypeOf(generatorProto)
console.log(originProto) //GeneratorFunction的原型对象
//原型链:generator -> generatorProto -> originProto
//可通过constructor获取GeneratorFunction构造函数
console.log(generatorProto.constructor) // GeneratorFunction?{prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction', constructor: ?}
可以结合下图来理解:由此可见,生成器对象的原型链上存在有next()方法,return()方法,以及throw()方法。我们可以通过对GeneratorFunction原型对象的属性遍历来获取这些方法:
for (const key of Object.getOwnPropertyNames(originProto)) {
console.log(key, originProto[key])
}
// constructor GeneratorFunction?{prototype: Generator, Symbol(Symbol.toStringTag): 'GeneratorFunction', constructor: ?}
// next ? next() { [native code] }
// return ? return() { [native code] }
// throw ? throw() { [native code] }
?补充一点知识:originProto的属性的enumerable都为false,因此无法用Object[keys | values | entries]获取,for-in循环更是如此。
可以获取不可遍历普通属性的方法为Object.getOwnPropertyNames,可以获取symbol属性的方法为Object.getOwnPropertySmybols;(Symbol属性无论是否可遍历,都无法用常规手段获取,如for-in, keys, values, entries等);
或者也可以使用Reflect反射的ownKeys方法,该方法可以获取所有自身的属性,包括Symbol。
关于属性的遍历,我会在下一篇博客中单独介绍。
next()方法
和迭代器对象的next()方法类似,都是迭代的执行顺序语句:
console.log(generator.next()) //{value: 1, done: false}
console.log(generator.next()) //{value: 2, done: false}
console.log(generator.next()) //{value: 3, done: false}
console.log(generator.next()) //{value: undefined, done: true}
调用next()方法生成的对象包含value属性和done属性,行为上也和迭代器生成对象一样。生成器的状态在调用next()方法的瞬间会变为执行态,在执行完毕后又会变为待执行或者终止状态。
return()方法
调用return()方法会提前终止生成器的迭代,使其提前进入终止状态,且不能再恢复之前的迭代,无法再继续执行。
console.log(generator.next()) //{value: 1, done: false}
console.log(generator.return()) //{value: undefined, done: true}
console.log(generator.next()) //{value: undefined, done: true}
console.log(generator.next()) //{value: undefined, done: true}
throw()方法
该方法会向生成器内部注入错误,如果再生成器执行之前(即调用next()之前)调用throw,则会将错误抛向外部;若生成器内部没有捕获(try/catch),则会将错误抛出:
function* genratorFn1() {
try {
yield 1
yield 2
} catch (error) {}
}
const generator1 = genratorFn1()
generator1.throw() //会抛出错误,在生成器执行之前调用throw
const generator2 = genratorFn1()
generator2.next()
generator2.throw() //不会抛出错误,在内部进行了捕获
function* genratorFn2() {
yield 1
yield 2
}
const generate3 = genratorFn2()
generator2.next()
generator2.throw() //会抛出错误,未在内部进行了捕获
你可以这样理解throw的执行机制:在上次执行的位置的下一句注入错误,并抛弃此代码块,忽略块级作用域内部的后序任何代码,并在注入之后自动执行一次迭代,去找寻下一个正常可执行的yield语句。
以下述为例:
function* genratorFn() {
try {
yield 1
yield 2
} catch (error) {}
try {
yield 3
yield 4
} catch (error) {}
try {
yield 5
} catch (error) {}
}
const generator = genratorFn()
console.log(generator.next())
//正常执行:{value: 1, done: false}
console.log(generator.throw())
//被生成器内部捕获,抛弃块作用域后序代码,并自动调用next()
//{value: 3, done: false}
console.log(generator.throw())
//没有可执行的yield语句,则生成器状态变为完成态
//{value: undefined, done: true}
console.log(generator.next())
//{value: undefined, done: true}
那如果最后一步的next()换成throw(),会发生什么事情呢?
答案是会将错误抛出。当这一行代码执行的时候,并没有try/catch语句捕获错误,这是因为在这之前的所有try/catch都已经被抛弃了。
利用这个机制,你可以巧妙的跳过某些执行阶段。比如:
function* genratorFn() {
try {
yield 1
yield 2
yield 3
yield 4
} catch (error) {}
try {
yield 'a'
yield 'b'
yield 'c'
yield 'd'
} catch (error) {}
}
const generator = genratorFn()
console.log(generator.next())
console.log(generator.next())
console.log(generator.throw())
console.log(generator.next())
//{value: 1, done: false}
//{value: 2, done: false}
//跳过了3,4
//{value: 'a', done: false}
//{value: 'b', done: false}
或者这样:
function* genratorFn() {
for (let i = 0; i < 4; i++) {
try {
yield i
} catch (error) {}
}
}
const generator = genratorFn()
console.log(generator.next())
generator.throw()
console.log(generator.next())
//{value: 0, done: false}
//{value: 2, done: false}
//此例中,并没有打印出throw的结果,另类的实现了跳过,但实际上,其本身依旧算是一次执行
效果上相同,但是原理有区别。最重要的是,一定要理解他的机制:将错误注入上一次执行过后的代码区域,并自动执行一次迭代。
3,生成器的行为
当执行到最后一个yield语句,或者遇到return语句的时候,便会将生成器的状态变为完成态。
如:
function* generatorFn() {
yield 1
return 2
yield 3
}
const generator = generatorFn()
console.log(generator.next())//{value: 1, done: false}
console.log(generator.next())//{value: 2, done: true}
console.log(generator.next())//{value: undefined, done: true}
或者:
function* generatorFn() {
yield 1
yield 2
}
const generator = generatorFn()
console.log(generator.next()) //{value: 1, done: false}
console.log(generator.next()) //{value: 2, done: true}
console.log(generator.next()) //{value: undefined, done: true}
结果是一样的。对其进行for-of循环,会与迭代器的循环类似:
for (const item of generator) {
console.log(item)
}
//1
//2
并且生成器也是一个一次性,不可逆的迭代对象。相关详情可参考上一篇迭代器中的介绍。
关于yield
yield关键字可以让生成器停止和开始执行。遇到关键字,则执行暂停,状态保留。并且yield关键字只能在生成器函数作用域内部使用。出现在其他作用域,或者子作用域,都会报错。
function *generatorFn(){
function fn(){
yield 1 //不合法!
}
}
不仅如此,yield还可以用来传参:
function* generatorFn() {
console.log(yield 1)
}
const generator = generatorFn()
generator.next('first')
generator.next('second')
可以思考一下,控制台会打印出什么结果。
答案是 ‘second’。因为yield用作传参有一个特性,就是本次调用next()传递的值,会赋给上一次执行的yield,也就是说第一次的传参‘first’,并没有‘上一个’yield去接收,因此是一个无效传参。并且第一次执行过后,生成器会停止在yield那一步,并不会开始执行console.log语句。
在观察一下下面的例子:
function* generatorFn() {
return yield 1
}
const generator = generatorFn()
console.log(generator.next('first'))
console.log(generator.next('second'))
我们知道,当生成器碰到return语句,便会将状态变为完成态。那么是否在此例中,第一次便会输出done为true呢?
其实并不会,因为return语句会等待其返回的值执行完毕,再最后执行。
比如代码 return x + 3 > 3,会等到后面的代码执行完运算和关系比较之后,再return一个Boolean值。
因此,其最后的返回结果应该是这样的:
//{value: 1, done: false}
//{value: 'second', done: true}
?留给大家一个思考题:
function* generatorFn() {
yield yield yield yield 'begin'
}
const generator = generatorFn()
console.log(generator.next(1))
console.log(generator.next(2))
console.log(generator.next(3))
console.log(generator.next(4))
console.log(generator.next(5))
上述例题会输出什么结果呢?
yield还有一个作用,就是使用 * 符,增强yield的行为,使其可以‘递归’生成器,或者迭代任何可迭代对象。
function* generatorFn(i) {
yield i
if (i < 3) {
yield* generatorFn(++i)
}
}
const generate = generatorFn(1)
console.log(generate.next()) //{value: 1, done: false}
console.log(generate.next()) //{value: 2, done: false}
console.log(generate.next()) //{value: 3, done: false}
console.log(generate.next()) //{value: undefined, done: true}
或者这样:
function* generatorFn(i) {
yield* [1, 2, 3]
}
const generate = generatorFn(1)
console.log(generate.next()) //{value: 1, done: false}
console.log(generate.next()) //{value: 2, done: false}
console.log(generate.next()) //{value: 3, done: false}
console.log(generate.next()) //{value: undefined, done: true}
yield*通俗的讲,其作用就是迭代一个可迭代对象,并将其每一个执行步骤都作为一个yield关键字处理。
这里会比较难懂,需要多手动操作加强理解。
4,生成器实现迭代接口
介绍完生成器的基础知识,下面再来介绍一下其的应用场景。
生成器最常见的最贴切的使用场景,就是用生成器来实现一个迭代接口。如下所示:
class MyArray extends Array {
*[Symbol.iterator]() {
for (let i = 0, j = this.length; i < j; i++) {
yield `myArray - ${this[i]}`
}
}
}
const myArr = new MyArray(1, 2, 3, 4)
for (const item of myArr) {
console.log(item)
}
// myArray - 1
// myArray - 2
// myArray - 3
// myArray - 4
或者,你可以实现一个自动flat的迭代接口:
class MyArray extends Array {
*[Symbol.iterator]() {
const queue = []
for (let i = 0, j = this.length; i < j; i++) {
queue[i] = this[i]
}
while (queue.length) {
const item = queue.shift()
if (Array.isArray(item)) {
queue.unshift(...item)
continue
}
yield item
}
}
}
const myArr = new MyArray(1, [2, [3, 4]], 5, 6)
for (const item of myArr) {
console.log(item) // 1,2,3,4,5,6
}
不过你依旧要注意,不能在迭代器内部调用任何与this相关的迭代语句,如Array.from,展开运算符,for-of等,否则会出现不必要的递归调用,从而导致爆栈问题。
或者,你也可以采用yield*的方式,更简洁的实现同样的功能:
class MyArray extends Array {
*[Symbol.iterator](target = this) {
for (let i = 0, j = target.length; i < j; i++) {
const item = target[i]
if (Array.isArray(item)) {
yield* this[Symbol.iterator](item)
} else {
yield item
}
}
}
}
const myArr = new MyArray(1, [2, [3, 4]], 5, 6)
for (const item of myArr) {
console.log(item) // 1,2,3,4,5,6
}
上述两个案例只是抛砖引玉,你可以使用生成器去实现更多更优雅的代码。
文中内容均带有个人理解,并不保证权威。若有错误,欢迎随时指正。
|