如何在JavaScript中使用生成器和产量

117 阅读6分钟

如何在JavaScript中使用生成器和产量

你知道JavaScript也有自己版本的生成器吗?实际上,很多开发JavaScript应用程序的人都不知道这个概念的存在,所以今天我们要介绍JavaScript中的生成器。


什么是生成器?

在ES6中,我们被引入了很多新的功能,比如箭头函数扩散运算符和生成器等等,但是什么是生成器呢?生成器是一个与普通函数相反的函数,它允许函数被退出,然后重新进入,其上下文(变量绑定)在重新进入时被保留下来。

让我们把它分解开来,一步一步地研究生成器,以便我们都能理解它的工作原理。当我们执行一个常规函数时,解释器将运行所有进入该函数的代码,直到该函数完成(或抛出一个错误)。这被称为 "运行-完成"模式。

让我们以一个非常简单的函数为例。

function regularFunction() {
    console.log("I'm a regular function")
    console.log("Surprise surprice")
    console.log("This is the end")
}

regularFunction()

-----------------
Output
-----------------
I'm a regular function
Surprise surprice
This is the end

还没有什么花哨的东西,就像你所期望的那样是一个普通的函数,它一直在执行,直到到达终点或返回一个值。但是,如果我们只是想在任何时候停止该函数,返回一个值,然后继续呢?这时,生成器就会进入画面。

我的第一个生成器函数

function* generatorFunction() {
    yield "This is the first return"
    console.log("First log!")
    yield "This is the second return"
    console.log("Second log!")
    return "Done!"
}

在我们执行该函数之前,你可能想知道一些事情,首先什么是function* ?这就是我们用来声明一个函数为生成器的语法。那么yield 呢?yield ,与返回不同,它将通过保存函数的所有状态来暂停函数,并在以后的连续调用中从该点继续。在这两种情况下,表达式将被返回到调用者的执行中。

我们的函数到底发生了什么?让我们通过调用该函数来了解一下。

generatorFunction()

-----------------
Output
-----------------
generatorFunction {<suspended>} {
    __proto__: Generator
    [[GeneratorLocation]]: VM272:1
    [[GeneratorStatus]]: "suspended"
    [[GeneratorFunction]]: ƒ* generatorFunction()
    [[GeneratorReceiver]]: Window
    [[Scopes]]: Scopes[3]
}

等等,什么?当我们调用生成器函数时,该函数不会被自动触发,相反,它会返回一个迭代器对象。这个对象的特别之处在于,当调用方法 next() 时,生成器函数的主体会被执行,直到第一个yieldreturn 表达式。让我们看看它的运行情况。

const myGenerator = generatorFunction()
myGenerator.next()

-----------------
Output
-----------------
{value: "This is the first return", done: false}

正如所解释的那样,生成器一直运行到第一个yield 语句,并产生一个包含value 属性的对象,和一个done 属性。

{ value: ..., done: ... }
  • value 属性等于我们产生的值
  • done 属性是一个布尔值,只有在生成器函数返回一个值后才会被设置为true 。(不是屈服的)

让我们再调用一次next() ,看看我们得到了什么

myGenerator.next()

-----------------
Output
-----------------
First log!
{value: "This is the second return", done: false}

这一次我们首先看到我们的生成器主体中的console.log 被执行,并打印出First log! ,以及第二个屈服的对象。我们还可以继续这样做。

myGenerator.next()

-----------------
Output
-----------------
Second log!
{value: "Done!", done: true}

现在第二个console.log 语句被执行,我们得到了一个新的返回对象,但是这一次属性done 被设置为true

done 属性的值不只是一个标志,它是一个非常重要的标志,因为我们只能对一个生成器对象进行一次迭代!。不相信吗?再试着调用一次next()

myGenerator.next()

-----------------
Output
-----------------
{value: undefined, done: true}

很好,它没有崩溃,但我们只得到了未定义,因为valuedone 属性仍然设置为真。

迭代器的屈服

在我们继续讨论一些场景之前,还有一个yield操作符的特殊性,那就是yield* 。让我们通过创建一个允许我们在一个数组上迭代的函数来解释它,天真地我们可以想到这样做。

function* yieldArray(arr) {
    yield arr
}

const myArrayGenerator1 = yieldArray([1, 2, 3])
myArrayGenerator1.next()

-----------------
Output
-----------------
{value: Array(3), done: false}

但这并不是我们想要的,我们想让数组中的每个元素都产生,所以我们可以尝试做这样的事情。

function* yieldArray(arr) {
    for (element of arr) {
        yield element
    }
}

const myArrayGenerator2 = yieldArray([1, 2, 3])
myArrayGenerator2.next()
myArrayGenerator2.next()
myArrayGenerator2.next()

-----------------
Output
-----------------
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}

现在我们得到了想要的结果,但我们可以做得更好吗?是的,我们可以。

function* yieldArray(arr) {
    yield* arr
}

const myArrayGenerator3 = yieldArray([1, 2, 3])
myArrayGenerator3.next()
myArrayGenerator3.next()
myArrayGenerator3.next()

-----------------
Output
-----------------
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}

很好,通过使用yield*表达式,我们可以遍历操作数,并产生它所返回的每个值。这适用于其他生成器、数组、字符串、任何可迭代对象。

现在你知道了JavaScript中生成器的所有情况,那么它们有什么用呢?


生成器的用途

生成器的好处是它们是懒惰评估的,这意味着调用next() 方法后返回的值是在我们特别要求的情况下才计算出来的。这使得生成器成为解决多种情况的好选择,比如下面介绍的情况。

生成一个无限的序列

正如我们在Python文章中所看到的,生成器很适合生成无限序列,这可能是任何东西,从素数到简单的计数。

function* infiniteSequence() {
    let num = 0
    while (true) {
        yield num
        num += 1
    }
}

for(i of infiniteSequence()) {
    if (i >= 10) {
        break
    }
    console.log(i)
}

-----------------
Output
-----------------
0
1
2
3
4
5
6
7
8
9

注意,在这种情况下,当i >= 10 ,我就退出循环,否则,它将永远运行下去(或直到手动停止)。

实现迭代器

当你需要实现一个迭代器时,你必须手动创建一个带有next() 方法的对象。此外,你还必须手动保存状态。

想象一下,我们想做一个迭代器,简单地返回I,am,iterable 。如果不使用生成器,我们将不得不做这样的事情。

const iterableObj = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step === 1) {
          return { value: 'I', done: false};
        } else if (step === 2) {
          return { value: 'am', done: false};
        } else if (step === 3) {
          return { value: 'iterable.', done: false};
        }
        return { value: '', done: true };
      }
    }
  },
}
for (const val of iterableObj) {
  console.log(val);
}

-----------------
Output
-----------------
I
am
iterable.

有了生成器,这就简单多了。

function* iterableObj() {
    yield 'I'
    yield 'am'
    yield 'iterable.'
}

for (const val of iterableObj()) {
  console.log(val);
}

-----------------
Output
-----------------
I
am
iterable.

更好的异步?

有些人认为生成器可以帮助改善承诺和回调的使用,尽管我更倾向于简单地使用 await/async。


注意事项

当我们使用生成器的时候,并不是所有的东西都是闪亮的。在设计上有一些限制,而且有两个非常重要的考虑。

  • 生成器对象只能一次性访问。一旦用完了,你就不能再对它进行迭代。要做到这一点,你将不得不创建一个新的生成器对象。
  • 生成器对象不允许随机访问,例如,数组。由于数值是一个一个生成的,你无法获得特定索引的数值,你将不得不手动调用所有的next() 函数,直到你到达想要的位置,但此时,你无法访问之前生成的元素。

总结

生成器函数对于优化我们的应用程序的性能很有帮助,也有助于简化构建迭代器所需的代码。

我希望你现在对JavaScript中的生成器有了很好的理解,并希望你能在你的下一个项目中使用它们。

谢谢你的阅读!