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!
"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):
- The first time it runs, it "pauses" on the first line and yields the string value
'✨'
- The second time it runs, it starts on the line of the previous
yield
keyword. It then runs all the way down till the secondyield
keyword and yields the value'💕'
. - 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! 😃
<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>