【JS】生成器与生成器对象

1 篇文章 0 订阅
1 篇文章 0 订阅

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
}

上述两个案例只是抛砖引玉,你可以使用生成器去实现更多更优雅的代码。

文中内容均带有个人理解,并不保证权威。若有错误,欢迎随时指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值