美文网首页
图解JavaScript生成器和迭代器

图解JavaScript生成器和迭代器

作者: xiaojichao | 来源:发表于2020-08-18 14:18 被阅读0次
    15959176127181.jpg <br />ES6引入了一个很酷的东西,叫做生成器(generator)函数。每当我问人们有关生成器函数的问题时,得到的回复基本上都是:“我曾看到过一次,没搞明白,然后就再也没有看到过”,“哦,天哪,我读过很多有关生成器函数的博客文章,但依然还没有搞明白”,“我是搞明白了,但是为什么有人会用它啊?”,🤔也许那只是我一直在与自己进行的对话,因为那是我很长一段时间以来习惯的思考方式!但是生成器函数是真的很酷。<br />
    <br />那么,什么是生成器函数呢?下面,我们先来看一个常规的老式函数。👵🏼<br /> 2ta7m1vxju6j1dzg7t7c.png <br />是的,这绝对没什么特别的!它只是一个普通函数,会输出一个值4次。下面我们来调用这个函数!<br />
    <br /> unsiscmakhlgxl4dcji7.gif <br />
    <br />“但是,为什么你要浪费我5秒钟的时间让我看看这个没劲的普通函数呢?”,这是一个很好的问题。普通函数遵循一种称为运行至完成的模型:当我们调用一个函数时,它将一直运行,直到完成为止(好吧,除非某处出错了)。我们不能随意在中间的某个位置暂停一个函数。<br />
    <br />现在最酷的部分来了:生成器函数不遵循运行到完成模型! 🤯这是否意味着我们可以在执行某个生成器函数时随机将其暂停呢?嗯,差不多吧!下面我们看看什么是生成器函数,以及如何使用它们。<br />
    <br />我们通过在关键字function后加上一个星号*,来创建一个生成器函数。<br /> 7j9pxfpvmecip71ldjwg.png <br />但这不是我们要用生成器函数所要做的全部!与常规函数相比,生成器函数实际上以完全不同的方式工作:<br />
    • 调用生成器函数会返回一个生成器对象,这个对象是一个迭代器。
    • 我们可以在生成器函数中使用yield关键字来暂停执行。

    <br />但这到底意味着什么?!<br />
    <br />我们先看看第一条:调用生成器函数将返回一个生成器对象。当调用常规函数时,函数体将被执行,并最终返回一个值。但是,当调用生成器函数时,会返回生成器对象!下面我们来看看输出生成器函数的返回值时是什么样子。<br />

    lyuivuuepy1hzpok8rc5.gif <br />现在,我可以听到你内心在尖叫(甚至发出声了🙃),因为这看起来有点让人难以承受。但是别担心,我们实际上不必用你在这里看到的任何属性。那么,生成器对象有什么好处呢?<br />
    <br />首先,我们需要退后一小步,并回答常规函数和生成器函数之间的第二个区别:我们可以在生成器函数中用yield关键字来“暂停”函数的执行。<br />
    <br />使用生成器函数,我们可以编写如下代码(genFuncgeneratorFunction的缩写):<br /> 1qo0upq0meh6gj2gs08o.png <br />这里的yield关键字是做什么的呢?当引擎遇到yield关键字的时候,生成器的执行将被“暂停”。而且最棒的是,下次运行该函数时,它会记住它先前暂停的位置,然后从那里开始运行!😃 那么,下面基本上就是运行该函数时要发生的事情(不要担心,稍后会对此进行动画演示):<br />
    1. 第一次运行时,它在第一行“暂停”,并生成字符串值'✨'
    2. 第二次运行时,它在上一个yield关键字的行开始,一直向下运行,直到遇到第二个yield关键字,并生成值'💕'
    3. 第三次运行时,它在上一个yield关键字的行开始,一直向下运行,直到遇到return关键字,然后返回值'Done!'

    <br />但是...我们在前面看到的是:如果调用生成器函数,返回的是一个生成器对象,那么如何调用这个生成器函数呢? 🤔 这就是生成器对象发挥作用的地方!<br />
    <br />生成器对象(在原型链上)包含一个next()方法。我们就是用这个方法来迭代生成器对象。但是,为了记住它之前在生成一个值后中断的地方的状态,我们需要将生成器对象赋值给一个变量。我会把这个变量简称为genObjgeneratorObject的简称)。<br />

    y54clkzwbc9oemzgybh5.gif <br /><br />是的,这和我们之前看到的一样可怕。下面我们看看在生成器对象genObj上调用next()方法时会发生什么!<br /> ryzc9gpzw4x5f0eqhzad.gif <br /><br />生成器会一直运行,直到遇到第一个yield关键字,而这里这个关键字恰好在第一行!它生成(yield)了一个包含value属性和done属性的对象{ value: ... , done: ... }:<br />
    • value属性等于我们生成的值。
    • done属性是一个布尔值,仅在生成器函数返回一个值(不是生成!😊)后,才设置为true

    <br />我们停止了遍历生成器,这让函数看起来像是暂停了一样!这多酷啊。下面我们再次调用next()方法!😃<br />

    e7hz87c6xtd31qjx19va.gif <br />首先,输出字符串First log!到控制台中。这条语句既没有yield也没有return关键字,所以引擎会继续往下执行!然后,遇到了一个yield关键字,其值为'💕'。这样就生成了一个对象,其value属性的值是'💕'done属性的值为false,因为我们还没有从生成器中返回。<br />
    <br />快结束了!下面我们最后一次调用next()。<br /> 33epxsx8znmhm0qojsuu.gif <br />字符串Second log!被输出到控制台。然后,引擎遇到一个返回值为'Done!'return关键字。引擎返回一个对象,该对象的value属性值为``'Done!'。这次我们是实际返回了,所以done属性值被设置为true!<br /> <br />done属性实际上非常重要。**我们只能一次迭代一个生成器对象**。什么?!那么当我们再次调用next()方法时会发生什么呢?<br />![wooo83by4eh12akmg5wb.gif](https://img.haomeiwen.com/i55810/a3e1a4893bd898fd.png)[<br />](media/15959149279378/wooo83by4eh12akmg5wb.gif)<br />它只是永远返回undefined`。如果要再次迭代,必须创建一个新的生成器对象!

    如我们所见,生成器函数返回一个迭代器(生成器对象)。但是..等等,迭代器?这是否意味着我们可以在返回的对象上用for...of循环以及扩展运算符呢?是的!🤩<br />
    <br />下面我们试着用[...]语法,将生成的值扩展到一个数组中。<br />

    xgk99j592vbx3qirw5or.gif <br />或者也许可以用for...of循环?!<br /> 98k242jz3bqorkjhukwl.gif <br />哎呀,这么多可能性!<br />
    <br />但是是什么让迭代器成为迭代器呢?我们之所以可以在数组、字符串、map和set上用for...of循环和扩展语法,实际上是因为它们实现了迭代器协议[Symbol.iterator]。假设我们有以下值(带有很具描述性的名称,哈哈💁🏼‍♀️):<br /> hs2sf1oj537c56yaej1h.png <br />arraystringgeneratorObject都是迭代器!下面我们来看看它们的[Symbol.iterator]属性的值。<br /> a7inxsrvrp8ykg3xw6zu.gif <br /><br />但是,不可迭代的值上的[Symbol.iterator]的值会是什么呢?<br /> tpuzuy58g8m7grxvqw8x.gif <br /><br />是的,不可迭代的值就没这个属性。那么..我们能不能在不可迭代的值上手动添加一个[Symbol.iterator]属性,让不可迭代的值变成可迭代的呢?是的,我们可以!😃<br />
    <br />[Symbol.iterator]必须返回一个迭代器,该迭代器包含一个next()方法,该方法返回一个对象,这个对象跟我们之前看到过的一样:{ value: '...', done: false/true }。<br />
    <br />为简单起见,我们可以简单地将[Symbol.iterator]的值设置为等于一个生成器函数,因为这样做默认就返回一个迭代器。下面我们让object变成可迭代的,并且让生成的值变成一个完整的对象:<br /> oysyy7v71o2q9q9mrcsx.png <br /><br />现在看看我们在我们的object对象上使用扩展语法或者for...of循环会发生什么!<br /> pw2qq1tkfbp8zccuecac.gif <br />或者也许我们只想获取对象的键。哦,那很容易,我们只需要生成Object.keys(this)而不是this!<br /> ankit4dn67unnwzfkv9y.png <br /><br />嗯,我们试一下。<br /> 75kf40lqcqrudzqgkeb7.gif <br />搞定!Object.keys(this)是个数组,所以生成的值也是一个数组。然后我们把这个生成的数组扩展到另一个数组中,结果就得到一个嵌套的数组。我们并不想要这个,只是想生成每个单独的键!<br />
    <br />好消息!🥳 我们可以用yield*关键字在生成器中的迭代器中生成单个值,所以yield带了一个星号!假设我们有一个生成器函数首先生成一个鳄梨,然后我们想分别生成另一个迭代器的值(在本例中是一个数组)。我们可以用yield*来做到这一点。然后我们委托给另一个生成器!<br /> jtyn5s5o3vdhjkbwwyb0.gif <br /><br />在继续迭代genObj迭代器之前,被委托的生成器的每个值都生成了。<br />
    <br />这正是为了单独得到所有对象的键所需要做的!<br /> btr4ytbb04c44qfs96v2.gif <br />

    生成器函数的另一种用法,是我们可以(在某种程度上)将它们用作为观察者函数。生成器可以等待传入的数据,并且只有当该数据被传递过来时,才会处理它。比如:<br />

    fts36exs5chxacikjeo3.png <br /><br />这里最大的区别是我们不是只有前一个例子中所看到的yield [value],而是将其赋值给一个称为second的值,并生成值字符串First!。这是第一次调用next()方法时会生成的值。<br />
    <br />下面我们来看看第一次在可迭代的值上调用next()方法时会发生什么。<br /> ob5a4yi79it9q2ben137.gif <br />首先遇到第一行上的yield,并生成值First!。那么,变量second的值是什么呢?<br />
    <br />变量second的值实际上是下次我们调用next()方法时传递给该方法的值!这一次,我们给next()方法传一个字符串'I like JavaScript'。<br /> l1840pp2k9h9bgpt1geo.gif <br /><br />重要的是,在这里看到,next()方法的第一次调用没有记录任何输入。我们只是通过第一次调用来启动观察器。生成器在继续之前,等待我们的输入,并可能处理我们传递给next()方法的值。

    那么,为什么我们会想用生成器函数呢?<br />
    <br />生成器的最大优点之一就是它们是懒求值的。也就是说,在调用next()方法后返回的值仅在我们明确要求后才计算!普通函数没有这种功能:所有值都为我们生成了,以防将来某个时间需要用到它。<br />

    7b24mkp7io3gmnn8pzwa.gif <br /><br />还有其它几个案例,但是我通常喜欢这样做,以便在迭代大型数据集时能更好地控制!<br />
    <br />想像一下,我们有一个读书俱乐部清单!📚为简单起见,每个读书俱乐部只有一个成员。一名会员目前正在读几本书,这些书以数组books来表示!<br /> 8opapd1iddlgj1ljixje.png <br /><br />现在,我们在找一本id为ey812的书。为找到这本书,我们可能只用一个嵌套的for循环或者一个forEach辅助器,但是这意味着我们得遍历所有数据,即使已经找到了要找的俱乐部会员之后也会继续遍历下去。<br />
    <br />关于生成器,很棒的事情是,除非我们告诉它去运行,否则它不会继续运行。也就是说,我们可以评估每个返回的条目,如果是我们正在找的条目,就不需要调用next()了。下面我们看看是啥样子。<br />
    <br />首先,创建一个生成器,遍历每个会员的books数组。我们把会员的books数组传给该函数,遍历该数组,并生成每本书!<br /> vokf28crwuvbmksd57m5.png <br />完美!现在我们必须创建一个遍历俱乐部会员数组(members)的生成器。我们实际上并不关心俱乐部会员本身,只是需要遍历他们的书。在iterateMembers生成器中,我们委托iterateBooks迭代器生成成员们的书!<br /> fy8mxxjj0uvs6rarm6mi.png <br />差不多了!最后一步是遍历读书俱乐部数组(bookclubs)。跟上个例子一样,我们实际上并不关心读书俱乐部本身,只关心俱乐部会员(特别是会员们的书)。下面我们委托给iterateMembers迭代器,并把clubMembers数组传给它。<br /> x1lor0omqw9t5k2kq4iv.png <br />为遍历所有这些东西,我们需要通过将bookClubs数组传递给iterateBookClubs生成器,让生成器对象可迭代。我现在暂时将其称为生成器对象it。<br /> omg23omwi8a1d7nn1it3.png <br /><br />下面我们调用next()方法,直到得到id为ey812的书。<br /> 72ghm4ev6el3no9esk1l.gif <br /><br />不错!现在为了得到要找的书,我们就不需要遍历所有数据,而是只按需查找数据!当然,每次都要手动调用next()方法效率不是很高,所以我们来做一个函数吧!<br />
    <br />下面将我们要找的书的id传给该函数。如果value.id是我们要找的id,那么就只返回整个value(book对象)。否则,如果不是要找的id,就再次调用next()!<br /> hxyeemfr3q8pqqotk51j.png <br /> x1zh0ygt5yfq5vb2f5at.gif <br /><br />当然,这只是一个很小很小的数据集。不过,试想一下,假如我们有成千上万的数据,或者可能是一个传入的流,为了只找到一个值,我们需要解析。通常,我们必须等待整个数据集准备就绪,才能开始解析。而用生成器函数,我们可以只需要小部分数据,检查该数据,然后只在我们调用next()方法时候才会生成值!

    如果你仍然处于懵逼状态,不要担心。虽然生成器函数确实相当难以理解,但是只要亲自用过它们,并且有一些可靠的案例,那就没那么难了。希望有些术语更清楚了点,跟往常一样:有任务疑问,请随时联系!😃<br />

    原文 by Lydia Hallie:https://dev.to/lydiahallie/javascript-visualized-generators-and-iterators-e36

    相关文章

      网友评论

          本文标题:图解JavaScript生成器和迭代器

          本文链接:https://www.haomeiwen.com/subject/itvcjktx.html