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 的返回值也是一个拥有 valuedone 属性的对象,value 值为 yield 后的表达式结果,默认为 undefineddone 的值代表生成器函数内的代码是否已经执行完毕,是则为 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())

执行结果如下图:

js 中的生成器函数与生成器

第 4 次执行 next 方法,代码从第 9 行开始执行,之后就结束了,所以返回的结果中 donetrue;因为没有指定返回值,相当于 return undefined,所以 valueundefined。如果我们提前定义了返回值,比如在例 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')
}

则打印结果会变成下图所示:

js 中的生成器函数与生成器

也就是说遇到 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())

执行的结果如下图:

js 中的生成器函数与生成器

可以发现,第 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())

打印结果如下:

js 中的生成器函数与生成器

生成器替代迭代器

既然生成器也是迭代器,那么我们就可以在一些场景里用生成器去替代迭代器。下面举 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 得到的结果如下:

js 中的生成器函数与生成器

我们可以将 makeIterator 变为生成器函数:

// 例 5.2
function* makeIterator(arr) {
  for (const item of arr) {
    yield item
  }
}

如此,每次执行 it.next() 返回的对象的 value值就是 yield 后面的 item,执行例 5.1 代码的结果如下,与原先例 5 一致,但是 makeIterator 的代码简洁了许多:

js 中的生成器函数与生成器

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
  }
}

注意,在类的声明中,如果要把某个方法变为生成器函数, * 直接写在方法名的前面。

js 中的生成器函数与生成器
js 中的生成器函数与生成器

原文链接:https://juejin.cn/post/7240081817722535995 作者:亦黑迷失

(0)
上一篇 2023年6月2日 上午11:15
下一篇 2023年6月3日 上午10:06

相关推荐

发表回复

登录后才能评论