Forewarning: this topic is hard! It's taken me a week to even start to get my head around it. You may have to read this a few times also, I have spent countless hours watching YouTube videos on this. I'll link one of my favourites at the end
What are you talking about?
Generators! An ES6 feature that is ultimately just a function to iterate over a series of values. However it has a bonus feature! What is it? I hear you ask. Well let me try to explain, When using a generator you can ultimately pause the execution of your code to do something and then return to it later in another clode block. They are very intimidating to start with but ultimately easy to understand after a while. The hardest bit for me was finding an example, or a few examples, that would just give me that glimmer as to why they are useful and why they are better than other possible ways of working (if I could find the examples showing both ways of working then awesome!). Normally I would look at the ES5 way's of working first, this time I am going to switch it up a bit, and we're going to look at the ES6 way to begine with!
So what does a generator look like
function * numberGenerator() {
yield 1
yield 2
yield 3
}
Notice the *
after the function keyword, that tells us this is a generator function. Then we have a new keyword yield
, this keyword is treated as though it's a mini version of return
inside the function.
function * numberGenerator() {
yield 1
yield 2
yield 3
}
const myNumbers = numberGenerator()
When you call a generator, as above, it will not start to do anything, it will be in a suspended state and it will return a generator object. Within the generator object there are 3 prototypes that can be called next()
, return()
, and throw()
. We'll start off by looking at the next()
prototype.
next() please!
When we call the next prototype, essentially what we are doing is telling the generator function to start and run until it hits a yield keyword, let's take a look at the example:
function * numberGenerator() {
yield 1
yield 2
yield 3
}
const myNumbers = numberGenerator()
console.log(myNumbers.next()) // This will return { value: 1, done: false } in a console log
Here we see that our code has started and run to the first yield of 1. The output of this give us an object with a value property and a done property, the done property will be false until after the last yield statement is seen
function * numberGenerator() {
yield 1
yield 2
yield 3
}
const myNumbers = numberGenerator()
console.log(myNumbers.next()) // This will return { value: 1, done: false } in a console log
console.log(myNumbers.next()) // This will return { value: 2, done: false } in a console log
console.log(myNumbers.next()) // This will return { value: 3, done: false } in a console log
console.log(myNumbers.next()) // This will return { value: undefined, done: true } in a console log
Above we now see that after we get through all the yields we see a value of undefined, with a done value of true. To make the code execution a bit clearer, we can add some log messages into out generator:
function * numberGenerator() {
console.log('Before 1')
yield 1
console.log('Before 2')
yield 2
console.log('Before 3')
yield 3
console.log('After 3')
}
const myNumbers = numberGenerator()
// We will see a console log stating "Before 1"
console.log(myNumbers.next()) // This will return { value: 1, done: false } in a console log
// We will see a console log stating "Before 2"
console.log(myNumbers.next()) // This will return { value: 2, done: false } in a console log
// We will see a console log stating "Before 3"
console.log(myNumbers.next()) // This will return { value: 3, done: false } in a console log
// We will see a console log stating "After 3"
console.log(myNumbers.next()) // This will return { value: undefined, done: true } in a console log
The above makes it a bit clearer to understand that when we first call .next()
we will enter our function, and execute up until the first yield, so we will output Before 1
and then { value: 1, done: false}
and so on.
So what about some use cases?
Before writing this article I wanted to try and find some example that would concrete my understanding of this topic, honestly I can't say that I fully understand it but here we are, trying things out, and perhaps you can help me understand more use cases?
Generate user ID's
function* generateId() {
let id = 1 // We could take this number from a database lookup
while (true) {
yield id
id++
}
}
const gen = generateId()
console.log(gen.next().value) // This would return 1 in a console log
console.log(gen.next().value) // This would return 2 in a console log
console.log(gen.next().value) // This would return 3 in a console log
In the above example, we use a while loop to make our generator an infinite loop always generating us the next number. The advantage here? Well, if you try to run a while(true)
loop in your own code, you will crash the browser in a few mere seconds and the only way to stop it will be to kill the browser processes on your PC (DO NOT TRY THIS!), doing this in a generator mean we only execute it one step at a time.
Can I pass parameters?
Yes, you can pass parameters into a generators next() function, and I must admit this bit stumped me for a while. To put it most simply, you can pass a parameter in, however if it is the first time calling .next()
it will not have any effect because you are yet to yield anything. The parameter sent to the .next()
essentially replaces the previous yielded. I'll try to explain with a code example below:
const maxScore = 5;
function* keepScore() {
let score = 0;
while (true) {
const addToScore = yield score // the first call to .next() will only run to here therefore returning 0
if(addToScore) { // addToScore is populated by the parameter you pass in after the first run
score += addToScore
}
}
}
const playerOne = keepScore()
console.log('score after first move: ')
console.log(playerOne.next()) // This would output 0
console.log('score after second move: ')
console.log(playerOne.next(3)) // This would output 3
console.log('score after third move: ')
console.log(playerOne.next(2)) // This would output 5
console.log('score after fourth move: ')
console.log(playerOne.next()) // This would output 5
console.log('score after fifth move: ')
console.log(playerOne.next(6)) // This would output 11
Making an early exit
With generators it is possible to exit from the function, this can be done in one of two ways. Firstly, you can call .return()
instead of next to make the generator exit, or you can use a return
statement inside the generator function itself. For example:
const maxCount = 50;
let hitMax = false;
function* countUp() {
let count = 0
while (true) {
const addToCount = yield count
if(addToCount) {
count += addToCount;
}
if(count >= maxCount){
hitMax = true;
return `maxCount has been hit or exceeded`
}
}
}
const counting = countUp();
counting.next();
for(let i=0; !hitMax; i++){
console.log(counting.next(i));
}
console.log("I am done")
Above we will keep counting until hitMax
is true, after which we will stop and exit our for
loop, in that example we return inside the generator. Let's look at an alternative:
const maxCount = 50;
let hitMax = false;
function* countUp() {
let count = 0
while (true) {
const addToCount = yield count
if(addToCount) {
count += addToCount;
}
if(count >= maxCount){
hitMax = true;
}
}
}
const counting = countUp();
counting.next();
for(let i=0; !counting.next().done; i++){
if(!hitMax){
console.log(counting.next(i));
} else {
console.log(counting.return('maxCount has been hit or exceeded'))
}
}
console.log("I am done")
Above we have to work slightly differently, we will keep incrementing until the done
value of .next()
is true, inside that loop we check our boolean of hitMax
and if we have hit that instead of counting again we will call .return('maxCount has been hit or exceeded')
which sets the .next().done
value to true and allows us to output a "completion" message.
Overview
WOW! This was by far the hardest topic I have looked at, and I think I have understood it to a basic level at least. The biggest challenge I found was finding and understand real-world use cases. I still don't think i have 100% cracked it with the example, perhaps you have something better? Feel free to share examples in the comments if you do :) The bigget point I learnt from this was:
A generator allows you to exit and re-enter the function multiple times until the done value is true, meaning you don't need to have multiple functions that you call at various stages through the lifecycle. Again, if you have a better explanation, hit me up!
Learn JavaScript Generators In 12 Minutes - Web Dev Simplified
Top comments (12)
I don’t understand why in the first 2 console log examples,
value: 3
gets printed twice.that'll just be a typo :) I'll get that fixed
You had one job! 😂😂😂 Jk, I was so confused though. Thanks for confirming!
Haha I know, I failed haha
Can also be used with
for...of
:Of course! I knew there was an asepct I missed out in my write up!!
Example use cases: 1) checksum calculation, 2) deliver content as it arrives (eg. from a server call), 3) break up collections in an efficient way, etc.
Great suggestions, I might have to look into some of them for further examples
Quão maravilhoso e aprender e com pessoas criativas o aprendizado fica melhor a cada artigo.
nota
const nota =()=>{
let number = '1000'
console.log(number, ('Parabéns'))
}
That's a huge one thanks 😊
This concept is sooo confusing but if its helped someone then I feel that I must have learnt something :)
Awesome Post! Thank you.