ES6 - A beginners guide - Generators

stefanwrightcodes

Stefan Wright

Posted on December 1, 2021

ES6 - A beginners guide - Generators

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
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
stefanwrightcodes
Stefan Wright

Posted on December 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related