如何在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() 时,生成器函数的主体会被执行,直到第一个yield 或return 表达式。让我们看看它的运行情况。
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}
很好,它没有崩溃,但我们只得到了未定义,因为value 和done 属性仍然设置为真。
迭代器的屈服
在我们继续讨论一些场景之前,还有一个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中的生成器有了很好的理解,并希望你能在你的下一个项目中使用它们。
谢谢你的阅读!