javascript图解之Generators 和 Iterators

ES6 引入了一些很酷的称为 generator (生成器)函数的东西🎉 每当我问人们有关生成器函数的问题时,他们的回答基本上是:“我见过他们一次,很困惑,再也没看过”,“哦,天哪,不,我读了那么多关于 generator 函数的博客文章,但还是没有搞定”,“我已经搞定了清楚了,但为什么有人会用这个”🤔或者也许这只是我和自己的对话,因为这是我过去很长一段时间的想法!但实际上他们很酷。
那么,什么是generator函数?我们先来看一个常规的老式函数👵🏼

是的,这没什么特别的!它只是一个正常的函数,记录一个值4次。让我们调用它!

“但是莉迪亚,你为什么让我看这个正常的无聊功能,浪费了我5秒钟的生命”,这是一个很好的问题。普通函数遵循一种称为“运行到完成”的模型:当我们调用一个函数时,它将一直运行到它完成为止(好吧,除非在某个地方出现错误)。我们不能随意地在中间的某个地方暂停一个函数。
现在有一个很酷的部分:generator 函数不遵循 run-to-completion 模型!🤯这是否意味着我们可以在执行 generator 函数的过程中随机暂停它?嗯,差不多吧!让我们看看什么是 generator 函数以及如何使用它们。
我们通过在函数关键字后面写一个星号  *  来创建 generator 函数。

 但这并不是我们使用生成器函数所要做的全部! generator 函数的工作方式与常规函数完全不同:

  • 调用 generator 函数返回生成器对象,该对象是迭代器。
  • 我们可以在生成器函数中使用 yield 关键字来“暂停”执行。

但那是什么意思!?
让我们先回顾一下第一个:调用 generator 函数返回生成器对象。当我们调用一个普通函数时,函数体被执行并最终返回一个值。但是,当我们调用 generator 函数时,会返回生成器对象!让我们看看当我们记录返回值时是什么样子。

现在,我可以听到你在内部(或外部)尖叫,因为这看起来有点不可理解OMG。但别担心,我们不需要您在这里看到的任何属性。那么生成器对象有什么好处呢?
首先,我们需要后退一小步,回答普通函数和 generator 函数之间的第二个区别:我们可以在生成器函数中使用 yield 关键字来“暂停”执行。
使用 generator 函数,我们可以编写如下内容( genFunc 是 generator Function 的缩写):

那个 yield 关键字在那里做什么?当遇到 yield 关键字时, generator 的执行将被“暂停”。最棒的是,下次我们运行函数时,它会记住以前暂停的位置,然后从那里运行!😃 基本上这里发生了什么(别担心稍后会有动画播放):

  1. 第一次运行时,它会在第一行“暂停”,并生成字符串值'✨'
  2. 第二次运行时,它从上一个 yield 关键字的行开始。然后它一直运行到第二个 yield 关键字并生成值'💕'.
  3. 第三次运行时,它从上一个 yield 关键字的行开始。它一直向下运行,直到遇到 return 关键字,并返回值 “Done!”。

但是。。。如果我们之前看到调用 generator 函数返回了 generator 对象,我们如何调用该函数?🤔这就是 generator 对象发挥作用的地方!
generator 对象包含 next 方法(在原型链上)。此方法将用于迭代生成器对象。但是,为了记住在产生值之后它先前停止的状态,我们需要将 generator 对象分配给一个变量。我称之为 genObj 是 generatorObject 的缩写。

是的,和我们以前看到的一样吓人。让我们看看在 genObj 生成器对象上调用下一个方法时会发生什么!

generator 一直运行,直到遇到第一个yield关键字,它恰好在第一行!它生成了一个包含值属性和完成属性的对象。
{value:。。。,done:。。。}
值属性等于我们得到的值
done 属性是一个布尔值,只有在 generator 函数 return 一个值(未生成)时才会将其设置为 true !😊).
我们停止了对 generator 的迭代,这使它看起来像是函数刚刚暂停!多酷啊。让我们再次调用下一个方法!😃

首先,我们打印了字符串到控制台。这既不是 yield 也不是 return 关键字,所以它会继续!然后,它遇到一个 yield 关键字,其值为'💕'。yield 对象的 value 属性为💕' 和一个 done 属性。 done 属性的值为 false ,因为我们还没有从这个生成器 return 。
我们快到了!让我们最后一次调用 next 。

我们记录了第二个日志!到控制台。然后,它遇到一个 return 关键字,值为 'Done!'。返回值属性为 “Done!”的对象。我们这次确实返回了,所以 done 的值设置为 true !
done 属性实际上非常重要。我们只能迭代 generator 对象一次。什么?!那么当我们再次调用 next 方法时会发生什么呢?

它只会永远返回未定义的值。如果你想再次迭代它,你只需要创建一个新的 generator 对象!
正如我们刚才看到的, generator 函数返回一个迭代器( generator 对象)。但是。。等等迭代器?这是否意味着我们可以使用for循环和返回对象上的 spread 运算符?啊!🤩
让我们尝试使用  […]  将产生的值分散到数组中的语法。

或者使用 for of 循环?!

太多的可能性了!
但是什么使迭代器成为迭代器呢?因为我们还可以使用of循环和带有数组、字符串、映射和集合的扩展语法。实际上是因为他们实现了迭代器协议: Symbol.iterator 。假设我们有以下值(具有非常描述性的名称哈哈💁🏼‍♀️):

数组、字符串和 generatorObject 都是迭代器!让我们看看它们的 [Symbol.iterator] 属性的值。

但是 [Symbol.iterator] 对不可迭代的值的值是多少?

是的,只是不在那里。所以。。我们可以简单地手动添加 [Symbol.iterator] 属性,并使不可 iterables 成为可 iterable 吗?是的,我们可以!😃
[Symbol.iterator] 必须返回一个迭代器,它包含一个 next 方法,该方法返回一个与我们之前看到的一样的对象:

{ value : “…”,done : false/true } 。
为了保持简单(就像 lazy me 喜欢做的那样),我们可以简单地将 [Symbol.iterator] 的值设置为一个 generator 函数,因为这在默认情况下返回一个迭代器。让我们将对象设为 iterable ,并将产生的值设为整个对象:

现在看看当我们在对象对象上使用扩展语法或 for-of 循环时会发生什么!

或者我们只想得到对象键。”哦,好吧,那很简单,我们只是 yield Object.keys(this) 而不是 this !

嗯,我们试试看。

哦,天啊。key (这个)是一个数组,所以得到的值是一个数组。然后我们将这个产生的数组扩展到另一个数组中,得到一个嵌套数组。我们不想这样,我们只想交出每一把钥匙!
好消息!🥳我们可以使用  yield*  关键字从生成器中的迭代器生成单个值,因此 yield 带有星号!假设我们有一个 generator 函数,它首先生成一个鳄梨,然后我们要分别生成另一个迭代器(在本例中是数组)的值。我们可以用  yield*  关键字来实现。然后我们委托给另一个生成器!

在继续迭代 genObj 迭代器之前,将生成委托生成器的每个值。
这正是我们需要做的,以获得所有的对象键个别!


generator 函数的另一个用途是,我们可以(某种程度上)将它们用作观察函数。生成器可以等待传入的数据,并且仅当该数据被传递时,它才会对其进行处理。例如:

这里的一个很大的区别是,我们不只是有 yield [value] ,就像我们在前面的例子中看到的。相反,我们指定一个名为 second 的变量,并首先 yield 一个字符串!“First”这是第一次调用 next 方法时将得到的值。
让我们看看当我们第一次调用 iterable 上的 next 方法时会发生什么。

它在第一行遇到了 yield 关键字,并首先生成了值为 'First' !。那么,变量 second 的值是多少?
这实际上是我们下次调用 next 方法时传递给它的值!这次,让我们传入字符串 “I like JavaScript ” 。

在这里必须看到,next 方法的第一次调用还没有跟踪任何输入。我们只是通过第一次调用观察者来启动它。生成器在继续之前等待我们的输入,并可能处理传递给下一个方法的值。


那么为什么要使用 generator 函数呢?
 generator 最大的优点之一是它们的惰性求值。这意味着在调用 next 方法后返回的值,只有在我们明确要求之后才计算出来!普通函数没有这个功能:所有的值都是为您生成的,以防将来需要使用它。

还有其他几个用例,但我通常喜欢这样做,以便在迭代大型数据集时有更多的控制权!
想象一下,我们有一个读书俱乐部清单!📚为了使本示例简短而不是一个巨大的代码块,每个读书俱乐部只有一个成员。一名成员当前正在阅读几本书,这些书在 books 数组中表示!

现在,我们在找一本 ID 为 ey812 的书。为了找到这一点,我们可以使用嵌套 for 循环或 forEach ,但这意味着即使在找到我们要找的 clubMembers 之后,我们仍然会遍历数据!
generator 的惊艳之处在于,除非我们告诉它,否则它不会继续运行。这意味着我们可以计算每个返回的item,如果它是我们正在寻找的item,我们只是不调用 next !让我们看看会是什么样子。
首先,让我们创建一个生成器,该生成器遍历books每个团队成员的数组。我们将团队成员的book数组传递给函数,遍历该数组,并生成每本书!

完美!现在,我们必须制作一个遍历 clubMembers 数组的生成器。我们并不真正在乎俱乐部成员本身,我们只需要遍历他们的书本即可。在 iterateMembers 生成器中,让我们委派 iterateBooks 迭代器以仅产生其书本!

差不多好了!最后一步是遍历书店。就像在前面的示例中一样,我们并不真正在乎读书俱乐部本身,我们只是在乎俱乐部会员(尤其是他们的书)。让我们委派 iterateClubMembers 迭代器,并将 clubMembers 数组传递给它。

为了遍历所有这些,我们需要通过将 bookClub 数组传递给 iterateBookClubs 生成器来获得生成器对象可迭代(iterable) 。我将只为迭代器调用generator对象 。

让我们调用 next 方法,直到我们得到一本 ID 为 ey812 的书。

真好!我们不必遍历所有数据即可获得我们想要的书。相反,我们只是按需查找数据!当然,next每次手动调用该方法都不是很有效。所以让我们做一个函数吧!

我们将一个传递 id 给函数,这是我们要查找的书的ID。如果 value.id 是我们要查找的id,则只需返回整个value(book对象)。否则,如果不正确 id ,请 next 再次调用!

当然,这是一个很小的数据集。但是,试想一下,我们有成千上万的数据,或者也许是我们需要解析以找到一个值的传入流。通常,我们必须等待整个数据集准备就绪才能开始解析。使用生成器函数,我们只需要一小部分数据,检查数据,然后仅在调用该 next 方法时才生成值!


如果你仍然抱着“到底发生了什么”的心态,不要担心,直到你自己使用了 generator 函数,并且有了一些可靠的用例,genetator函数才变得相当混乱!我希望现在有些术语能更清楚一点,并且一如既往:如果您有任何问题,请随时联系!😃

译者言:阮一峰大大的ES6入门也有关于Generator和Iterator的详细解释,可以参考:Generator 函数的语法Iterator 和 for...of 循环

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值