原文地址:https://dev.to/lydiahallie/javascript-visualized-generators-and-iterators-e36
ES6引入了一个很酷的东西,叫做生成器函数 🎉,每当我问别人关于生成器函数的问题,回答基本上都是: "我看过一次,很困惑,就没再看了","哦,天哪,我读过很多关于生成器函数的博客文章,但我还是不明白","我明白了,但为什么有人会用这个"🤔或者这只是我和自己的对话,因为我过去很长一段时间都是这么想的!"!但它们实际上是相当酷的。
那么,什么是生成器函数?让我们先来看一个常规的老式函数 👵🏼
是的,这绝对没什么特别的!这只是一个打印 4 次值的普通函数。我们来调用一下它!
“但是,Lydia,为什么你浪费了我5秒钟的时间,只是看这个普通无聊的函数?”这是一个非常好的问题。普通函数遵循一种称为“运行至完成模型”的模型:当我们调用一个函数时,它将一直运行直到完成(好吧,除非在某处发生错误)。我们不能随意在函数中间的任何位置暂停函数。
现在最酷的部分来了:生成器函数不遵循运行到完成模型! 🤯 这是否意味着我们可以在生成器函数执行过程中随机暂停它?好吧,有点!让我们来看看生成器函数是什么以及我们如何使用它们。
我们通过在 function 关键字后写一个星号 * 来创建一个生成器函数。
但这并不是我们使用生成器函数所要做的全部!与常规函数相比,生成器函数实际上以完全不同的方式工作:
- 调用生成器函数返回一个生成器对象,它是一个迭代器。
- 我们可以在生成器函数中使用 yield 关键字来“暂停”执行。
但这到底是什么意思!?
让我们先回顾一下第一个:调用生成器函数返回一个生成器对象。当我们调用一个常规函数时,函数体被执行并最终返回一个值。然而,当我们调用生成器函数时,会返回一个生成器对象!让我们打印一下返回值的样子。
现在,我可以听到你内心(或外部 🙃)的尖叫,因为这看起来有点令人不知所措。但别担心,我们真的不必使用您在此处看到的任何记录的属性。那么生成器对象有什么用呢?
首先我们需要退一步,回答常规函数和生成器函数的第二个区别:我们可以在生成器函数中使用 yield 关键字来“暂停”执行。
使用生成器函数,我们可以这样写( genFunc 是 generatorFunction 的缩写):
那个 yield
关键字在做什么?生成器的执行在遇到 yield
关键字时“暂停”。最好的事情是,下次运行函数时,它记住了之前暂停的位置,并从那里继续运行!😃这基本上就是这里发生的事情(不用担心,稍后会进行动画演示):
- 第一次运行时,它在第一行 "暂停" 并产生字符串值 "✨"。
- 第二次运行时,它从上一个 yield 关键字的行开始。然后一直运行到第二个 yield 关键字,并产生值’💕’。
- 第三次运行时,它从上一个 yield 关键字的那一行开始。它一直向下运行,直到遇到 return 关键字,并返回值’完成!’。
但是…如果我们之前看到调用生成器函数返回了一个生成器对象,我们该如何调用该函数呢?🤔 这就是生成器对象发挥作用的地方!
生成器对象包含一个Next方法(在原型链上)。这个方法是我们用来迭代生成器对象的。然而,为了在产生一个值后记住它之前所处的状态,我们需要将生成器对象分配给一个变量。我把它叫做genObj,是generatorObject的缩写。
是的,就是我们之前看到的那个看起来可怕的东西。让我们看看在genObj生成器对象上调用 next 方法时会发生什么!
生成器一直运行到遇到第一个 yield 关键字,而这个关键字恰好在第一行!它产生了一个包含 value
属性和一个 done
属性的对象。
{ value: ... , done: ... }
"value" 属性等于我们生成的值。"done" 属性是一个布尔值,只有当生成器函数返回一个值(而不是生成值!😊)时才设置为 true。
我们停止了对生成器的迭代,这使得函数看起来就像暂停了一样!这是多么酷啊。让我们再次调用next方法!😃
首先,我们将字符串“First log!”记录到控制台中。这既不是 yield 关键字也不是 return 关键字,因此它继续执行!然后,它遇到了一个带有值“💕”的 yield 关键字。一个对象被 yield,其值属性为“💕”,并带有一个 done 属性。done 属性的值为 false,因为我们还没有从生成器中返回。
我们快到了!让我们最后一次调用下一个。
我们将字符串 Second log! 记录到控制台。然后,它遇到了值为 ‘Done!’ 的 return 关键字。返回的对象具有 ‘Done!’ 的 value 属性。我们这次实际上返回了,所以 done 的值设置为 true !
done 属性实际上非常重要。我们只能迭代一次生成器对象。什么?!那么,当我们再次调用 next 方法时会发生什么?
它只是永远返回 undefined 。如果你想再次迭代它,你只需要创建一个新的生成器对象!
正如我们刚刚看到的,生成器函数返回一个迭代器(生成器对象)。但。。等待迭代器?这是否意味着我们可以在返回的对象上使用 for of 循环和扩展运算符?亚斯!🤩
让我们尝试使用 [… ] 语法将生成的值分散到数组中。
或者也许通过使用 for of 循环?!
但是是什么让迭代器成为迭代器呢?因为我们还可以将 for-of 循环和扩展语法与数组、字符串、映射和集合一起使用。这实际上是因为他们实现了迭代器协议: [Symbol.iterator] 。假设我们有以下值(具有非常描述性的名称,哈哈💁🏼 ♀️):
array 、 string 和 generatorObject 都是迭代器!让我们看一下他们的 [Symbol.iterator] 属性的值。
但是,不可迭代的值上的 [Symbol.iterator] 值是多少?
是的,它只是不存在。所以。。我们可以简单地手动添加 [Symbol.iterator] 属性,并使不可迭代对象可迭代吗?是的,我们能!😃
[Symbol.iterator] 必须返回一个迭代器,其中包含一个 next 方法,该方法返回一个对象,就像我们之前看到的那样: { value: ‘…’, done: false/true } 。
为了保持简单(就像懒惰的我喜欢做的那样),我们可以简单地将 [Symbol.iterator] 的值设置为等于生成器函数,因为这默认返回一个迭代器。让我们使对象成为可迭代对象,并将生成的值设置为整个对象:
看看现在我们在 object 对象上使用 spread 语法或 for-of 循环会发生什么!
或者,也许我们只想获取对象键。“哦,这很容易,我们只产生 Object.keys(this) 而不是 this ”!
嗯,让我们试试。
哦,射击。 Object.keys(this) 是一个数组,因此生成的值是一个数组。然后我们将这个生成的数组分散到另一个数组中,从而得到一个嵌套数组。我们不想要这个,我们只是想交出每个单独的密钥!
好消息!🥳 我们可以使用 yield* 关键字从生成器中的迭代器中生成单个值,因此带有星号的 yield !假设我们有一个生成器函数,它首先产生一个鳄梨,然后我们想单独产生另一个迭代器(在本例中为数组)的值。我们可以使用 yield* 关键字来做到这一点。然后我们委托给另一个生成器!
委托生成器的每个值都会在继续迭代 genObj 迭代器之前生成。
这正是我们需要做的,以便单独获取所有对象键!
生成器函数的另一个用途是,我们可以(某种程度上)将它们用作观察器函数。生成器可以等待传入的数据,并且只有当该数据被传递时,它才会处理它。举个例子:
这里一个很大的区别是,我们不只是像前面的例子中看到的那样有 yield [value] 。相反,我们分配一个名为 second 的值,并生成字符串 First! 的值。这是我们第一次调用 next 方法时将生成的值。
让我们看看当我们第一次在可迭代对象上调用 next 方法时会发生什么。
它在第一行遇到 yield ,并生成值 First! 。那么,变量 second 的值是多少?
这实际上是我们下次调用时传递给 next 方法的值!这次,让我们传递字符串 ‘I like JavaScript’ 。
请务必在此处看到, next 方法的第一次调用尚未跟踪任何输入。我们只需通过第一次调用观察器来启动它。生成器在继续之前等待我们的输入,并可能处理我们传递给 next 方法的值。
那么,为什么要使用生成器函数呢?
生成器的最大优点之一是它们是懒惰评估的。这意味着调用 next 方法后返回的值只有在我们特别要求它之后才会计算!普通函数没有这个:所有值都是为你生成的,以防你将来需要使用它。
还有其他几个用例,但我通常喜欢这样做,以便在迭代大型数据集时获得更多控制!
想象一下,我们有一个读书俱乐部的名单!📚 为了使此示例简短,而不是一个巨大的代码块,每个读书俱乐部只有一个成员。一个成员当前正在阅读几本书,这些书在 books 数组中表示!
现在,我们正在寻找一本 ID 为 ey812 的书。为了找到它,我们可能只使用嵌套的 for 循环或 forEach 帮助程序,但这意味着即使在找到我们正在寻找的团队成员之后,我们仍然会遍历数据!
发电机的奇妙之处在于,除非我们告诉它,否则它不会继续运行。这意味着我们可以评估每个返回的项目,如果它是我们要查找的项目,我们根本不调用 next !让我们看看那会是什么样子。
首先,让我们创建一个生成器,用于遍历每个团队成员的 books 数组。我们将团队成员的 book 数组传递给函数,遍历数组,并生成每本书!
完美!现在我们必须创建一个遍历 clubMembers 数组的生成器。我们并不真正关心俱乐部成员本身,我们只需要遍历他们的书。在 iterateMembers 生成器中,让我们委托 iterateBooks 迭代器,以便只生成它们的书籍!
快到了!最后一步是遍历读书俱乐部。就像前面的例子一样,我们并不真正关心读书俱乐部本身,我们只关心俱乐部成员(尤其是他们的书)。让我们委托 iterateClubMembers 迭代器并将 clubMembers 数组传递给它。
为了遍历所有这些,我们需要通过将 bookClub 数组传递给 iterateBookClubs 生成器来获取生成器对象可迭代。我现在只调用生成器对象 it ,用于迭代器。
让我们调用 next 方法,直到我们得到一本 id 为 ey812 的书。
好!我们不必为了得到我们想要的书而遍历所有数据。相反,我们只是按需查找数据!当然,每次手动调用 next 方法并不是很有效…因此,让我们创建一个函数!
让我们将 id 传递给函数,这是我们要查找的书籍的 ID。如果 value.id 是我们正在寻找的 id,则只需返回整个 value (book 对象)。否则,如果不是正确的 id ,请再次调用 next !
当然,这是一个很小的数据集。但想象一下,我们有成吨的数据,或者我们需要解析一个传入的流才能找到一个值。通常,我们必须等待整个数据集准备就绪,才能开始解析。使用生成器函数,我们可以简单地要求小块数据,检查该数据,并且只有在调用 next 方法时才生成值!
如果您仍然处于“到底发生了什么”的心态,请不要担心,生成器功能在您自己使用它们并有一些可靠的用例之前非常混乱!我希望现在有些条款更清楚一点,并且一如既往:如果您有任何问题,请随时与我们联系!😃