JavaScript之生成器

JavaScript之生成器

看红宝书+查资料,重新梳理JavaScript的知识。

生成器是一个函数的形式,通过在函数名称前加一个星号(*)就表示它是一个生成器。所以只要是可以定义函数的地方,就可以定义生成器

functiongFn({ }

const gFn = function* ({ }

const o = {
    * gFn() { }
}

箭头函数不能用来定义生成器函数,因为生成器函数使用 function*语法编写。

那为啥上面的第三个例子可以不使用 function*语法呢?

因为那个是简写版本。等价于:

const o = {
    gFnfunction* ({ }
}

生成器的简单使用

functiongFn({
    console.log(111)
}

gFn()

然后,我们会很”开心”地发现控制台没有打印任何信息。

这是因为调用生成器函数会产生一个生成器对象,但是这个生成器一开始处于暂停执行的状态,需要调用 next方法才能让生成器开始或恢复执行。

return会直接让生成器到达 done: true状态

functiongFn({
    console.log(111)
    return 222
}

const g = gFn()
console.log(g)

console.log(g.next())
console.log(g.next())
JavaScript之生成器
image-20220416114721721

有种迭代器的既视感。实际上,生成器实现了Iterable接口,它们默认的迭代器是自引用的。

functiongFn({
    console.log(111)
    return 222
}

const g = gFn()

console.log(gFn()[Symbol.iterator]())   // gFn {<suspended>}
console.log(g === g[Symbol.iterator]()) // true

也就是说,生成器其实就是一个特殊的迭代器

yield关键字

提到生成器,自然不能忘记 yield关键字。 yield能让生成器停止,此时函数作用域的状态会被保留,只能通过在生成器对象上调用 next方法来恢复执行。

上面我们已经说了,** return会直接让生成器到达 done: true状态,而 yield则是让生成器到达 done: false状态,并停止**

functiongFn({
  yield 111
  return 222
}

const g = gFn()

console.log(g.next())
console.log(g.next())
JavaScript之生成器
image-20220416162345189

简单分析一下:

JavaScript之生成器
image-20220416163137033

另外,** yield关键字只能在生成器内部使用,用在其他地方会抛出错误。**

生成器拥有迭代器的特性

正如上面所说,生成器是特殊的迭代器,所以生成器也拥有迭代器的特性。

生成器对象之间互不干扰

functiongFn({
    yield 'red';
    yield 'blue';
    return 'purple';
}

const g1 = gFn()
const g2 = gFn()

console.log(g1.next())
console.log(g2.next())
JavaScript之生成器
image-20220416164330795

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

functiongFn({
  yield 'red'
  yield 'blue'
  yield 'purple'
  return 'white'
}

const g = gFn()

for (const i of g) {
  console.log(i)    // 依次输出red、blue、purple
}

return的内容不能被迭代。

完成但并不完成

functiongFn({
    yield 'red';
    return 'blue';
    yield 'purple';
}


let g = gFn()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
JavaScript之生成器
image-20220416165321695

return会让当前这一次的 next调用得到 value: return的数据, done: true的结果,此时生成器已经完成了,但是还能继续调用 next方法,只是返回的结果都会是 {value: undefined, done: true},即使后面还有 yield关键字或 return关键字。

使用yield实现输入和输出

functiongFn(initial{
  console.log(initial)
}

const g = gFn('red')
console.log(g.next())
JavaScript之生成器
image-20220416231233648

yield关键字作为函数的中间参数使用

首先,生成器肯定是能够传参的,因为生成器是一个特殊的函数。只不过它不是一调用就执行的,它需要通过调用生成器对象的 next方法才能开始执行。

那么,如果 next方法传参应该怎么接收参数呢?

yield关键字会接收传给 next方法的第一个值。

直接来实践比较好理解。

functiongFn(initial{
  console.log(initial)
  console.log(yield)
  console.log(yield)
  console.log(yield)
}

const g = gFn('red')
g.next('white')
g.next('blue')
g.next('purple')
JavaScript之生成器
image-20220416232423112
  1. 生成生成器,此时处于暂停执行的状态
  2. 调用 next,让生成器开始执行,输出 red,然后准备输出 yield,发现是 yield,暂停执行,出去外面一下。
  3. 外面给 next方法传参 blue,又恢复执行,然后之前暂停的地方(即 yield)就会接收到 blue。然后又遇到 yield暂停。
  4. 又恢复执行,输出 purple

然后,可能就会有人问,第一次传的 white怎么消失了?

它确实消失了,因为第一次调用 next方法是为了开始执行生成器函数,而刚开始执行生成器函数时并没有 yield接收参数,所以第一次调用 next的值并不会被使用。

yield关键字同时用于输入和输出

yield可以和 return同时使用,同时用于输入和输出

functiongFn({
  yield 111
  return yield 222
}

const g = gFn()

console.log(g.next(333))
console.log(g.next(444))
console.log(g.next(555))
JavaScript之生成器
image-20220417091733574
  1. 生成生成器,此时处于暂停执行的状态
  2. 调用 next,让生成器开始执行,遇到 yield,暂停执行,因为 yield后面还有111,所以带着111作为输出出去外面。
  3. 调用 next,生成器恢复执行,遇到 return,准备带着后面的数据跑路,结果发现后面是 yield,所以又带着222,作为输出到外面。
  4. 调用 next,又又又恢复执行,不过这个时候return的内容是 yield表达式,所以 yield会作为输入接收 555,然后再把它带到外面去输出。

** yield表达式需要计算要产生的值,如果后面没有值,那就默认是 undefined**。 return yield x的语法就是,遇到 yield,先计算出要产生的值 111,在暂停执行的时候作为输出带出去,然后调用 next方法时, yield又作为输入接收 next方法的第一个参数。

产生可迭代对象

上面已经提过了,生成器是一个特殊的迭代器,那么它能怎样产生可迭代对象呢?

functiongFn({
  for (const x of [123]) {
    yield x
  }
}

const g = gFn()

for (const x of g) {
  console.log(x)    // 1、2、3
}

如上所示,利用 yield能让生成器暂停执行的特性。

但是呢?我们还可以使用 *来加强 yield的行为,让它能迭代一个可迭代对象,从而一次产生一个值。这样子就能让我们能更简单的产生可迭代对象。

functiongFn({
  yield* [123]
}

const g = gFn()

for (const x of g) {
  console.log(x)    // 1、2、3
}

那么能不能不用 *呢?

functiongFn({
  yield [123]
}

const g = gFn()

for (const x of g) {
  console.log(x)    // [1, 2, 3]
}

不用 *的话,就会变成只有一个数组的迭代对象。

下面再来尝试一下 *的用法

functioninnerGFn({
  yield 111
  yield 222
  return 333
}

functionouterGFn({
  console.log('iter value: 'yield* innerGFn())
}

for (const x of outerGFn()) {
  console.log('value: ', x)
}
JavaScript之生成器
image-20220417094830971
  1. 上面有两个生成器函数,首先需要拿到 outerGFn生成器产生的可迭代对象去迭代。
  2. 然后发现 outerGFn也需要拿到 innerGFn产生的可迭代对象,去迭代,再产生一个给最外面迭代的可迭代对象
  3. 所以最外面的迭代结果会是 111 222,而 outerGFn的输出则是 innerGFn返回的值

利用 yield*实现递归算法

利用 yield*可以实现递归操作,此时生成器可以产生自己。

话不多说,开干

functionnTimes(n{
    if (n > 0) {
        // debugger
        yield* nTimes(n - 1)
        yield n - 1
    }
}

for (const x of nTimes(5)) {
    console.log(x)
}
JavaScript之生成器
image-20220417100611148
  1. 首先,需要迭代nTimes(5)产生的可迭代对象
  2. 进入到 nTimes里,又需要一直 yield* nTimes(n-1),一直递归自己,到n===0为止
  3. n===0,不满足条件不再继续递归,回退到上一层,此时n==1,执行 yield n - 1
  4. 依次回退到上一层,执行 yield n - 1,最终 nTimes(5)产生的可迭代对象内的值就是0, 1, 2, 3 ,4

提前终止生成器

生成器也能和迭代器一样提前终止,不过和迭代器的不太一样。

可以通过return throw两种方法,提前终止生成器,都会强制生成器进入关闭状态(generatorFn {<closed>})。

一旦进入关闭状态,之后再调用next()都会显示 done: true状态。

return

functiongFn({
  yield 111
  yield 222
  yield 333
}

const g = gFn()

console.log(g.next())

console.log(g.return(99))

console.log(g)

console.log(g.next(11))
console.log(g.next(22))
console.log(g.return(88))
JavaScript之生成器
image-20220417102018496

当我们调用生成器的 return方法时,生成器会进入关闭状态,后续再调用 next方法,都会显示 done: true状态,且 value也只有在再次调用 return的时候才能得到不是 undefined的值。

for-of循环会忽略状态为 done: true的,即如果提前终止生成器,那么实际上就相当于退出 for-of循环

functiongFn({
  yield 111
  yield 222
  yield 333
}

const g = gFn()

for (const x of g) {
  console.log(x)

  if (x === 222) {
    console.log(g.return(444))
    console.log(g)
  }
}
JavaScript之生成器
image-20220417103100765

throw

throw也可以提前终止生成器,且会抛出异常,需要捕获处理抛出的异常。

functiongFn({
  yield 111
  yield 222
  yield 333
}

const g = gFn()
// g.throw(444)       // 如果异常没有被处理的话,会直接报错

try {
  g.throw(444)
catch (e) {
  console.log(e)
}

console.log(g)

console.log(g.next())
JavaScript之生成器
image-20220417103544691

不过,如果处理异常是在生成器内部的话,情况就不太一样了。

functiongFn({
  for (const x of [123]) {
    try {
      yield x
    } catch (e) {
      console.log(e)
    }
  }
}

const g = gFn()

console.log(g.next())

g.throw(444)
console.log(g)

console.log(g.next())
JavaScript之生成器
image-20220417104645347

如果是在生成器内部处理这个错误,那么生成器不会关闭,还可以恢复执行,只是会跳过对应的yield,即会跳过一个值。

throw语句会得到被跳过的那个值

console.log(g.throw(444))
JavaScript之生成器
image-20220417104806188

以下是个人看法(有错请指出),处理异常在生成器内部的话,还是会正常终止生成器的,只是在生成器内部的循环中处理异常才会不终止外部的生成器。原因不晓得。

functiongFn({
  try {
    yield 1
    yield 2
    yield 3
  } catch (e) {
    console.log(e)
  }
}

const g = gFn()

console.log(g.next())

console.log(g.throw(444))
console.log(g)

console.log(g.next())
JavaScript之生成器
image-20220417105316091

参考资料:

  • 红宝书
  • MDN


原文始发于微信公众号(赤蓝紫):JavaScript之生成器

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/45481.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!