💡🎁 JavaScript 可视化:生成器和迭代器

2025-05-25

💡🎁 JavaScript 可视化:生成器和迭代器

ES6 引入了一种很酷的东西,叫做生成器函数🎉每当我问别人关于生成器函数的问题时,他们的回答基本上都是:“我见过一次,但感到很困惑,就再也没看过了”、“天哪,我读过很多关于生成器函数的博客文章,但还是不明白”、“我明白了,但为什么有人会用它”🤔或者也许这只是我一直以来的自言自语,因为我以前很长一段时间都是这么想的!但它们实际上很酷。

那么,什么是生成器函数?我们先来看一个普通的、老式的函数👵🏼

是的,这没什么特别的!它只是一个普通的函数,会打印 4 次值。让我们调用它吧!

替代文本

“但是 Lydia,你为什么让我看这个无聊透顶的函数,却浪费了我 5 秒钟呢?”这个问题问得真好。普通函数遵循所谓的“运行至完成”模型:当我们调用一个函数时,它会一直运行直到完成(当然,除非出现错误)。我们不能随意地在某个地方暂停一个函数。

现在最酷的部分来了:生成器函数不遵循“运行至完成”模型!🤯 这是否意味着我们可以在生成器函数执行过程中随机暂停它?嗯,有点像!让我们看看什么是生成器函数以及如何使用它们。

我们通过在关键字*后面写一个星号来创建一个生成器函数function

但这并不是使用生成器函数所要做的全部!与常规函数相比,生成器函数的工作方式实际上完全不同:

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

但这到底意味着什么?

我们先来回顾一下第一个:调用生成器函数会返回一个生成器对象。当我们调用普通函数时,函数体会被执行并最终返回一个值。然而,当我们调用生成器函数时,会返回一个生成器对象!让我们看看打印返回值时是什么样子的。


现在,我能听到你内心(或者说是外表🙃)的尖叫,因为这看起来有点让人不知所措。不过别担心,我们实际上并不需要用到你在这里看到的任何属性。那么,生成器对象有什么用呢?

首先,我们需要退一步,回答常规函数和生成器函数之间的第二个区别:我们可以yield在生成器函数中使用关键字来“暂停”执行

使用生成器函数,我们可以像这样写(genFunc是 的缩写generatorFunction): 那个关键字在那里做什么?生成器的执行在遇到关键字时会“暂停”。最棒的是,下次我们运行该函数时,它会记住上次暂停的位置,并从那里继续运行!😃 基本上就是这样的(别担心,稍后会制作动画演示):

yieldyield

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

但是……如果我们之前看到调用生成器函数返回的是一个生成器对象,那么该如何调用该函数呢?🤔 这时,生成器对象就派上用场了!

生成器对象包含一个next方法(在原型链上)。我们将使用这个方法来迭代生成器对象。但是,为了记住它在产生一个值后上次中断的状态,我们需要将生成器对象赋值给一个变量。我将其简称genObjgeneratorObject

是的,和我们之前看到的那个看起来很吓人的对象一样。让我们看看当我们调用生成器对象next上的方法时会发生什么genObj

生成器一直运行,直到遇到第一个yield关键字,而这个关键字恰好在第一行!它生成了一个包含一个value属性和一个done属性的对象。

```{

值:...,完成:... }


The `value` property is equal to the value that we yielded.
The `done` property is a boolean value, which is only set to `true` once the generator function _*returned*_ a value (not yielded! 😊). 

We stopped iterating over the generator, which makes it look like the function just paused! How cool is that. Let's invoke the `next` method again! 😃

<img src="https://thepracticaldev.s3.amazonaws.com/i/e7hz87c6xtd31qjx19va.gif" /> 

First, we logged the string `First log!` to the console. This is neither a `yield` nor `return` keyword, so it continues! Then, it encountered a `yield` keyword with the value `'💕'`. An object gets _yielded_ with the `value` property of `'💕'` and a `done` property. The value of the `done` property is `false`, since we haven't _returned_ from the generator yet. 

We're almost there! Let's invoke `next` for the last time. 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/33epxsx8znmhm0qojsuu.gif)

We logged the string `Second log!` to the console.  Then, it encountered a `return` keyword with the value `'Done!'`. An object gets returned with the `value` property of `'Done!'`. We actually _returned_ this time, so the value of `done` is set to `true`! 

The `done` property is actually very important. **We can only iterate a generator object _once_.** What?! So what happens when we call the `next` method again? 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/wooo83by4eh12akmg5wb.gif)

It simply returns `undefined` forever. In case you want to iterate it again, you just have to create a new generator object!

---

As we just saw, a generator function returns an iterator (the generator object). But.. wait an _iterator_? Does that mean we can use `for of` loops, and the spread operator on the returned object? Yas! 🤩

Let's try to spread the yielded values in an array, using the `[... ]` syntax.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/xgk99j592vbx3qirw5or.gif)

Or maybe by using a `for of` loop?!

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/98k242jz3bqorkjhukwl.gif)

Heck so many possibilities! 

But what makes an iterator an iterator? Because we can also use `for-of` loops and the spread syntax with arrays, strings, maps, and sets. It's actually because they implement the _iterator protocol_: the `[Symbol.iterator]`. Say that we have the following values (with very descriptive names lol 💁🏼‍♀️):
<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/hs2sf1oj537c56yaej1h.png" />

The `array`, `string`, and `generatorObject` are all iterators! Let's take a look at the value of their `[Symbol.iterator]` property.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/a7inxsrvrp8ykg3xw6zu.gif)

But then what's the value of the `[Symbol.iterator]` on the values that aren't iterable? 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/tpuzuy58g8m7grxvqw8x.gif)

Yeah, it's just not there. So.. Can we simply just add the `[Symbol.iterator]` property manually, and make non-iterables iterable? Yes, we can! 😃

`[Symbol.iterator]` has to return an iterator, containing a `next` method which returns an object just like we saw before: `{ value: '...', done: false/true }`. 

To keep it simple (as lazy me likes to do) we can simply set the value of `[Symbol.iterator]` equal to a generator function, as this returns an iterator by default. Let's make the object an iterable, and the yielded value the entire object:
<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/oysyy7v71o2q9q9mrcsx.png" />

See what happens when we use the spread syntax or a for-of loop on our `object` object now!

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/pw2qq1tkfbp8zccuecac.gif)

Or maybe we only wanted to get the object keys. "Oh well that's easy, we just yield `Object.keys(this)` instead of `this`"! 
<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/ankit4dn67unnwzfkv9y.png" />

Hmm let's try that.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/75kf40lqcqrudzqgkeb7.gif)

Oh shoot. `Object.keys(this)` is an array, so the value that got yielded is an array. Then we spread this yielded array into another array, resulting in a nested array. We didn't want this, we just wanted to yield each individual key! 

Good news! 🥳 We can yield individual values from iterators within a generator using the `yield*` keyword, so the `yield` with an asterisk! Say that we have a generator function that first yield an avocado, then we want to yield the values of another iterator (an array in this case) individually. We can do so with the `yield*` keyword. We then _delegate_ to another generator! 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/jtyn5s5o3vdhjkbwwyb0.gif)

Each value of the delegated generator gets yielded, before it continued iterating the `genObj` iterator. 

This is exactly what we need to do in order to get all object keys individually! 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/btr4ytbb04c44qfs96v2.gif)

---

Another use of generator functions, is that we can (sort of) use them as observer functions. A generator can wait for incoming data, and only if that data is passed, it will process it. An example:
<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/fts36exs5chxacikjeo3.png" />

A big difference here is that we don't just have `yield [value]` like we saw in the previous examples. Instead, we assign a value called `second`, and yield value the string `First!`. This is the value that will get yielded the first time we call the `next` method.

Let's see what happens when we call the `next` method for the first time on the iterable. 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/ob5a4yi79it9q2ben137.gif)

It encountered the `yield` on the first line, and yielded the value `First!`. So, what's the value of the variable `second`? 

That's actually the value that we pass to the `next` method the _next time we call it_! This time, let's pass the string `'I like JavaScript'`.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/l1840pp2k9h9bgpt1geo.gif)

It's important to see here that the first invocation of the `next` method doesn't keep track of any input yet. We simply start the observer by invoking it the first time. The generator waits for our input, before it continues, and possibly processes the value that we pass to the `next` method. 

 ---

So why would you ever want to use generator functions? 

One of the biggest advantages of generators is the fact that they are **lazily evaluated**. This means that the value that gets returned after invoking the `next` method, is only computed after we specifically asked for it! Normal functions don't have this: all the values are generated for you in case you need to use it some time in the future. 

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/7b24mkp7io3gmnn8pzwa.gif)

There are several other use cases, but I usually like to do it to have way more control when I'm iterating large datasets! 

Imagine we have a list of book clubs! 📚 To keep this example short and not one huge block of code, each book club just has one member. A member is currently reading several books, which is represented in the `books` array!

<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/8opapd1iddlgj1ljixje.png" />

Now, we're looking for a book with the id `ey812`. In order to find that, we could potentially just use a nested for-loop or a `forEach` helper, but that means that we'd still be iterating through the data even after finding the team member we were looking for!

The awesome thing about generators, is that it doesn't keep on running unless we tell it to. This means that we can evaluate each returned item, and if it's the item we're looking for, we simply don't call `next`! Let's see what that would look like.

First, let's create a generator that iterates through the `books` array of each team member. We'll pass the team member's `book` array to the function, iterate through the array, and yield each book!

<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/vokf28crwuvbmksd57m5.png" />

Perfect! Now we have to make a generator that iterates through the `clubMembers` array. We don't really care about the club member itself, we just need to iterate through their books. In the `iterateMembers` generator, let's delegate the `iterateBooks` iterator in order to just yield their books!

<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/fy8mxxjj0uvs6rarm6mi.png" />

Almost there! The last step is to iterate through the bookclubs. Just like in the previous example, we don't really care about the bookclubs themselves, we just care about the club members (and especially their books). Let's delegate the `iterateClubMembers` iterator and pass the `clubMembers` array to it.

<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/x1lor0omqw9t5k2kq4iv.png" />

In order to iterate through all this, we need to get the generator object iterable by passing the `bookClub` array to the `iterateBookClubs` generator. I'll just call the generator object `it` for now, for iterator.

<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/omg23omwi8a1d7nn1it3.png" />

Let's invoke the `next` method, until we get a book with the id `ey812`.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/72ghm4ev6el3no9esk1l.gif)

Nice! We didn't have to iterate through all the data in order to get the book we were looking for. Instead, we just looked for the data on demand! of course, calling the `next` method manually each time isn't very efficient... So let's make a function instead! 

Let's pass an `id` to the function, which is the id of the book we're looking for. If the `value.id` is the id we're looking for, then simply return the entire `value` (the book object). Else, if it's not the correct `id`, invoke `next` again!  
<img width="500" src="https://thepracticaldev.s3.amazonaws.com/i/hxyeemfr3q8pqqotk51j.png" />

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/x1zh0ygt5yfq5vb2f5at.gif)

Of course this was a tiny tiny data set. But just imagine that we have tons and tons of data, or maybe an incoming stream that we need to parse in order to just find one value. Normally, we'd have to wait for the entire dataset to be ready, in order to begin parsing. With generator functions, we can simply require small chunks of data, check that data, and the values are only generated when we invoke the `next` method! 

---

Don't worry if you're still all "what the heck is happening" mindset, generator functions are quite confusing until you've used them yourself and had some solid use cases for it! I hoped some terms are a bit clearer now, and as always: if you have any questions, feel free to reach out! 😃

<table>
  <tr>
 <td>✨ <a href="https://www.twitter.com/lydiahallie">Twitter</a></td>
<td>👩🏽‍💻 <a href="https://www.instagram.com/theavocoder">Instagram</a></td>
<td>💻 <a href="https://www.github.com/lydiahallie">GitHub</a></td>
<td>💡 <a href="https://www.linkedin.com/in/lydia-hallie">LinkedIn</a></td>
<td>📷 <a href="https://www.youtube.com/channel/UC4EWKIKdKiDtAscQ9BIXwUw">YouTube</a></td>
<td>💌 <a href=mailto:lydiahallie.dev@gmail.com">Email</a></td>

</tr>
</table>
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/lydiahallie/javascript-visualized-generators-and-iterators-e36
PREV
🔥🕺🏼 JavaScript 可视化:提升
NEXT
✨♻️ JavaScript 可视化:事件循环