JavaScript生成器(generator)-深入

159 阅读4分钟

前言

本篇主要记录一些生成器的语法细节和使用场景

语法细节

生成器对象作为可迭代对象

直接调用生成器对象的next()方法用处不大,可以直接将生成器对象作为可迭代对象去使用

function *genFn() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

const gen = genFn()
console.log(gen.next()) // {value: 1, done: false}
console.log(gen.next()) // {value: 2, done: false}
console.log(gen.next()) // {value: 3, done: false}
console.log(gen.next()) // {value: 4, done: true}

for (const i of genFn()) {
  console.log(i) // 1, 2, 3
}

console.log([...genFn()]) // [1, 2, 3]

上面的生成器函数genFn返回的生成器对象可以直接被原生结构for-of以及...运算符迭代,但是使用时有几个值得注意的点

  1. 生成器函数内部的执行流程会针对每个生成器对象区分作用域,也就是每个生成器对象之间不会相互影响
  2. yield关键字只能在生成器函数内部使用
  3. 生成器函数的返回值不会被迭代,只会作为最后一次执行是返回迭代结果对象的value

使用yield实现输入和输出

上一次让函数暂停的yield关键字会接收到下一次调用next()传递的第一个参数值,但是传递给第一次调用next()的值不会被接收,因为第一次是让生成器函数开始执行,但是第一个yield产出的值会做为第一个next()返回的迭代器结果对象的值

function* genFn(x) {
  console.log(x) // x
  console.log(yield 1) // b
  console.log(yield 1) // c
  console.log(yield 1) // d
  return 4
}

const gen = genFn('x')
console.log(gen.next('a')) // {value: 1, done: false}
console.log(gen.next('b')) // {value: 2, done: false}
console.log(gen.next('c')) // {value: 3, done: false}
console.log(gen.next('d')) // {value: 4, done: true}

可以看到第一次调用next()传递的值a不会被接收到

产生可迭代对象

可以通过*号增强yield关键字,让它去迭代一个可迭代对象,从而一次产生一个值

function* genFn(x) {
  yield* [1, 2, 3]
  yield* [4, 5, 6]
}

console.log([...genFn()]) // [1, 2, 3, 4, 5, 6]

yield其实就是把可迭代对象的数值转化为一连串可以单独产出的值,yield接收到的值是迭代器返回 done:true 时的value,对于普通迭代器那么这个值就是undefined,如果是生成器那么这个值就是这个生成器函数的返回值

function* genFoo() {
  yield 'a'
  return 'b'
}

function* genFn(x) {
  console.log('迭代器yield的值:', yield* [1, 2, 3]) // undefined
  console.log('生成器yield的值:', yield* genFoo()) // 'b'
}

console.log([...genFn()]) // [1, 2, 3, 'a']

提前终止生成器

生成器和迭代器一样也有关闭这个概念,生成器对象上原生实现了可选的return()和throw()方法,调用后可强制生成器进入关闭状态

  1. return() 我们可以主动调用return()方法让生成机进入关闭状态,而且关闭之后就无法恢复了,继续调用next()方法只会显示done:true的状态

在使用for-of等内置语言结构时,关闭生成器时的值也就是迭代器返回结果对象{done:true, value: 'xxx'}会被忽略

function* genFn() {
  yield* [1, 2, 3, 4, 5]
}

const gen = genFn()
console.log(gen) // genFn {<suspended>}
console.log(gen.return(8)) // {value: 8, done: true}
console.log(gen) // genFn {<closed>}
console.log(gen.next()) // {value: undefined, done: true}

const genFoo = genFn()
for (const i of genFoo) {
  console.log(i) // 1, 2, 3
  if(i > 2) {
    console.log(genFoo.return(8)) // {value: 8, done: true}
  }
}
  1. throw() throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误没有被处理,生成器就会关闭
function* genFn() {
  yield* [1, 2, 3, 4, 5]
}

const gen = genFn()
console.log(gen) // genFn {<suspended>}
try {
  gen.throw('error')
} catch (error) {
  console.log(gen) // genFn {<closed>}
}

不过如果我们在生成器内部处理了这个错误,那么生成器就不会关闭,而且还可以继续执行,错误处理会跳过对应的yield,所以会少一个输出值

function* genFn() {
  for (const i of [1, 2, 3, 4, 5]) {
    try {
      yield i
    } catch (e) {
      console.log(e)
    }
  }
}

const gen = genFn()
console.log(gen) // genFn {<suspended>}
for (const i of gen) {
  console.log(i) // 产出了 2 之后抛出错误,3不会被产出,所以输出结果为 1, 2, 'error', 4, 5
  if (i === 2) {
    gen.throw('error')
  }
}
console.log(gen) // genFn {<closed>}

使用场景

生成器作为默认迭代器

在实例的原型上面实现Symbol.iterator,并且定义为生成器函数,那么返回的生成器对象可以作为迭代器使用

class Foo {
  constructor() {
    this.value = [1, 2, 3]
  }

  *[Symbol.iterator]() {
    yield* this.value
  }

}

for (const i of new Foo()) {
  console.log(i) // 1, 2, 3
}

使用yield*实现递归

function *nTimes(n) {
  if(n > 0) {
    yield* nTimes(n - 1)
    yield n - 1
  }
}

for (const i of nTimes(10)) {
  console.log(i)
}

总结

本文主要介绍了生成器的一些语法细节和一些使用案例,如果想要对生成器有更加深入,更加系统的了解,可以看看文末推荐的参考文献,后续也会出一篇生成器深入的博客

参考文献