6 💡🎁 JavaScript Visualized: Generators and Iterators


6 💡🎁 JavaScript Visualized: Generators and Iterators

Cover image for 💡🎁 JavaScript Visualized: Generators and Iterators

ES6 introduced something cool called generator functions 🎉 Whenever I ask people about generator functions, the responses are basically: “I’ve seem them once, got confused, never looked at it again”, “oh gosh no I’ve read so many blog posts about generator functions and I still don’t get them”, “I get them but why would anyone ever use that” 🤔 Or maybe that’s just the conversations I’ve been having with myself because that’s how I used to think for a long time! But they’re actually quite cool.

So, what are generator functions? Let’s first just look at a regular, old-fashioned function 👵🏼

Yep absolutely nothing special about this! It’s just a normal function that logs a value 4 times. Let’s invoke it!

Alt Text

“But Lydia why did you just waste 5 seconds of my life by making me look at this normal boring function”, a very good question. Normal functions follow something called a run-to-completion model: when we invoke a function, it will always run until it completes (well, unless there’s an error somewhere). We can’t just randomly pause a function somewhere in the middle whenever we want to.

Now here comes the cool part: generator functions don’t follow the run-to-completion model! 🤯 Does this mean that we can randomly pause a generator function in the middle of executing it? Well, sort of! Let’s take a look at what generator functions are and how we can use them.

We create a generator function by writing an asterisk * after the function keyword.

But that’s not all we have to do to use generator functions! Generator functions actually work in a completely different way compared to regular functions:

  • Invoking a generator function returns a generator object, which is an iterator.
  • We can use the yield keyword in a generator function to “pause” the execution.

But what does that even mean!?

Let’s first go over the first one: _Invoking a generator function returns a generator object_. When we invoke a regular function, the function body gets executed and eventually returns a value. However when we invoke a generator function, a generator object gets returned! Let’s see what that looks like when we log the returned value.


Now, I can hear you screaming internally (or externally 🙃) because this can look a little overwhelming. But don’t worry, we don’t really have to use any of the properties you see logged here. So what’s the generator object good for then?

First we need to take a small step back, and answer the second difference between regular functions and generator functions: We can use the yield keyword in a generator function to “pause” the execution.

With generator functions, we can write something like this (genFunc is short for generatorFunction):

What’s that yield keyword doing there? The execution of the generator gets “paused” when it encounters a yield keyword. And the best thing is that the next time we run the function, it remembered where it previously paused, and runs from there on! 😃 Basically what’s happening here (don’t worry this will be animated later on):

  1. The first time it runs, it “pauses” on the first line and yields the string value '✨'
  2. The second time it runs, it starts on the line of the previous yield keyword. It then runs all the way down till the second yield keyword and yields the value '💕'.
  3. The third time it runs, it start on the line of the previous yield keyword. It runs all the way down until it encounters the return keyword, and returns the value 'Done!'.

But… how can we invoke the function if we previously saw that invoking the generator function returned a generator object? 🤔 This is where the generator object comes into play!

The generator object contains a next method (on the prototype chain). This method is what we’ll use to iterate the generator object. However, in order to remember the state of where it previously left off after yielding a value, we need to assign the generator object to a variable. I’ll call it genObj short for generatorObject.

Yep, the same scary looking object as we saw before. Let’s see what happens when we invoke the next method on the genObj generator object!

The generator ran until it encountered the first yield keyword, which happened to be on the first line! It yielded an object containing a value property, and a done property.

{ 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! 😃

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

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

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

Or maybe by using a for of loop?!

Alt Text

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 💁🏼‍♀️):

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

Alt Text

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

Alt Text

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:

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

Alt Text

Or maybe we only wanted to get the object keys. “Oh well that’s easy, we just yield Object.keys(this) instead of this“!

Hmm let’s try that.

Alt Text

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

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


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:

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

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

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

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!

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!

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!

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.

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.

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

Alt Text

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!

Alt Text

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! 😃


文章作者: Lydia Hallie
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lydia Hallie !
  目录