JavaScript之生成器

简介: JavaScript之生成器
看红宝书+查资料,重新梳理JavaScript的知识。

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

function* gFn() { }

const gFn = function* () { }

const o = {
    * gFn() { }
}

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

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

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

const o = {
    gFn: function* () { }
}

生成器的简单使用

function* gFn() {
    console.log(111)
}

gFn()

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

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

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

function* gFn() {
    console.log(111)
    return 222
}

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

console.log(g.next())
console.log(g.next())

image-20220416114721721

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

function* gFn() {
    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状态,并停止

function* gFn() {
  yield 111
  return 222
}

const g = gFn()

console.log(g.next())
console.log(g.next())

image-20220416162345189

简单分析一下:

image-20220416163137033

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

生成器拥有迭代器的特性

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

生成器对象之间互不干扰

function* gFn() {
    yield 'red';
    yield 'blue';
    return 'purple';
}

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

console.log(g1.next())
console.log(g2.next())

image-20220416164330795

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

function* gFn() {
  yield 'red'
  yield 'blue'
  yield 'purple'
  return 'white'
}

const g = gFn()

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

return的内容不能被迭代。

完成但并不完成

function* gFn() {
    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())

image-20220416165321695

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

使用yield实现输入和输出

function* gFn(initial) {
  console.log(initial)
}

const g = gFn('red')
console.log(g.next())

image-20220416231233648

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

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

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

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

直接来实践比较好理解。

function* gFn(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')

image-20220416232423112

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

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

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

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

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

function* gFn() {
  yield 111
  return yield 222
}

const g = gFn()

console.log(g.next(333))
console.log(g.next(444))
console.log(g.next(555))

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方法的第一个参数。

产生可迭代对象

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

function* gFn() {
  for (const x of [1, 2, 3]) {
    yield x
  }
}

const g = gFn()

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

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

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

function* gFn() {
  yield* [1, 2, 3]
}

const g = gFn()

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

那么能不能不用 *呢?

function* gFn() {
  yield [1, 2, 3]
}

const g = gFn()

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

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

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

function* innerGFn() {
  yield 111
  yield 222
  return 333
}

function* outerGFn() {
  console.log('iter value: ', yield* innerGFn())
}

for (const x of outerGFn()) {
  console.log('value: ', x)
}

image-20220417094830971

  1. 上面有两个生成器函数,首先需要拿到 outerGFn生成器产生的可迭代对象去迭代。
  2. 然后发现 outerGFn也需要拿到 innerGFn产生的可迭代对象,去迭代,再产生一个给最外面迭代的可迭代对象
  3. 所以最外面的迭代结果会是 111 222,而 outerGFn的输出则是 innerGFn返回的值

利用 yield*实现递归算法

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

话不多说,开干

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

for (const x of nTimes(5)) {
    console.log(x)
}

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

function* gFn() {
  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))

image-20220417102018496

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

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

function* gFn() {
  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)
  }
}

image-20220417103100765

throw

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

function* gFn() {
  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())

image-20220417103544691

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

function* gFn() {
  for (const x of [1, 2, 3]) {
    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())

image-20220417104645347

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

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

console.log(g.throw(444))

image-20220417104806188

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

function* gFn() {
  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())

image-20220417105316091

参考资料:

  • 红宝书
  • MDN
目录
相关文章
|
前端开发 JavaScript 中间件
掌握JavaScript中的迭代器和生成器(下)
掌握JavaScript中的迭代器和生成器(下)
115 0
|
存储 JavaScript 前端开发
掌握JavaScript中的迭代器和生成器(上)
掌握JavaScript中的迭代器和生成器
208 0
|
5月前
|
移动开发 前端开发 JavaScript
征信报告修改器,征信报告生成器,制作软件无痕修改软件【js+html+css】
本项目为信用评分模拟器教学工具,采用HTML5实现,仅供学习参考。核心功能通过JavaScript构建,包含虚拟数据生成、权重分配及信用因素分析(如还款记录、信用使用率等)。
|
5月前
|
存储 自然语言处理 前端开发
抖音快手小红书虚拟评论截图生成器,模拟对话制作工具,html+js+css
这是一款纯前端实现的多平台虚拟评论生成器,支持抖音、快手、小红书风格,适用于产品演示与UI设计。采用Vanilla JS与Flexbox布局,利用IndexedDB存储数据,CSS Variables切换主题。
|
5月前
|
存储 前端开发 安全
病历单生成器在线制作,病历单生成器app,HTML+CSS+JS恶搞工具
本项目为医疗病历模拟生成器,旨在为医学教学和软件开发测试提供数据支持,严格遵守《医疗机构病历管理规定》。
|
5月前
|
前端开发 容器
处方单图片生成器, 处方单在线制作免费,js+css+html恶搞神器
这是一个电子处方模拟生成系统,使用html2canvas库实现图片导出功能。系统生成的处方单包含多重防伪标识,并明确标注为模拟数据,仅供学习
|
5月前
|
前端开发 JavaScript 容器
制作b超单生成器, 假怀孕b超单图片制作, p图医院证明【css+html+js装逼恶搞神器】
本资源提供一个适合用于熟人之间恶搞的工具,效果逼真,仅供学习参考与娱乐。包含前端技术学习要点:语义化布局、响应式设计、Flexbox、图片自适应
|
存储 JavaScript 前端开发
javascript中的生成器和迭代器是什么
JavaScript中的生成器和迭代器是处理集合数据的利器,它们提供了一种遍历和操作元素的统一方式。迭代器是具有`next()`方法的对象,返回包含`value`和`done`属性的对象,用于循环处理集合。生成器函数更进一步,可以在执行过程中暂停并返回值,通过`yield`产生迭代值,适用于生成序列、异步编程和实现状态机等场景。例如,一个生成器可以无限生成斐波那契数列,或者在读取文件时控制异步流程。使用这些工具,代码变得更简洁、高效。
156 0