How Javascript Generator Functions Work

Generators are functions that can stop halfway through execution, and then continue from where they stopped when you call them again. Even though they act differently from regular functions, they are still callable. Let’s look at how they work.

How Generator Functions Work in Javascript

Let’s look at a normal function first. In this example, we run a while loop up to 100, and return its value:

function generator() {
    let current = 0;
    while(current < 100) {
        current = current + 1;
    }
    return current;
}

console.log(generator);

If we run this, we will get a return value of 100. If we were to move the return statement into the while() look, it would return 1 instead. In fact, every time we run it, it will return 1.

Use Cases for a Generator Function

This is great for some use cases – but in others, it’s not so useful:

  • Imagine you didn’t need to go all the way to 100 every time – some users only needed to go to 55. In this case, this function is quite inefficient, since it does more than what is needed.
  • Or maybe we need to pause the while loop when a user does a certain action – with this function, we can’t do that. In both cases, a function that could stop when we wanted it to, is more memory efficient.
  • That’s where generator functions come in. Instead of writing return, we can use yield, to pause the iteration and return a single value. It also remembers where we left off so that we can continue iterating through each item.

Let’s convert our function to a generator:

function* generator() {
    let current = 0;
    while(current < 100) {
        current = current + 1;
        yield current;
    }
}

let runGenerator = generator();
console.log(runGenerator.next()); // Returns { value: 1, done: false }
console.log(runGenerator.next()); // Returns { value: 2, done: false }
console.log(runGenerator.next()); // Returns { value: 3, done: false }
console.log(runGenerator.next()); // Returns { value: 4, done: false }
console.log(runGenerator.next()); // Returns { value: 5, done: false }

We’ve introduced two new concepts to our function: first, we’ve written function* Instead of a function, and when we ran our function, we use a method called next().

Function* And Yield

function* tells Javascript that this function is a generator. When we define a generator, we have to use the yield keyword, to return any values ​​from it. We’ve used a while loop above and that ultimately defines 100 yield statements, but we can also manually type yield multiple times, and each time the code will go to the next yield:

function* generator() {
    yield 1;
    yield 2;
    yield 3;
}

let runGenerator = generator();
console.log(runGenerator.next()); // Returns { value: 1, done: false }
console.log(runGenerator.next()); // Returns { value: 2, done: false }
console.log(runGenerator.next()); // Returns { value: 3, done: false }

yield can also return objects and arrays, like so:

function* generator() {
    let current = 0;
    while(current < 100) {
        let previous = current;
        current = current + 1;
        yield [ current, previous ]
    }
}

let runGenerator = generator();
console.log(runGenerator);
console.log(runGenerator.next()); // Returns { value: [ 1, 0 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 2, 1 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 3, 2 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 4, 3 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 5, 4 ], done: false }

next()

Any generator function you run will have a next() method attached to it. If you try to run the generator function and console log it without next() you’ll get the message generator { <suspended> }.

The next() method returns some data on the current state of the generator, in the form { value: value, done: status }, where value is the current value the generator is returning, and status is whether or not it’s completed.

If we had a smaller generator, where we only checked for numbers below 5, done would eventually return true:

function* generator() {
    let current = 0;
    while(current < 5) {
        let previous = current;
        current = current + 1;
        yield [ current, previous ]
    }
}

let runGenerator = generator();
console.log(runGenerator);
console.log(runGenerator.next()); // Returns { value: [ 1, 0 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 2, 1 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 3, 2 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 4, 3 ], done: false }
console.log(runGenerator.next()); // Returns { value: [ 5, 4 ], done: false }
console.log(runGenerator.next()); // Returns { value: undefined, done: true }

This lets us easily check if a generator is complete or not.

Changing the Yield Value

If we pass a value to next()it uses that value in the place of an yield expression. For example, consider the following:

function* generator() {
    let current = 0;
    while(current < 5) {
        current = yield current + 1;
    }
}

let runGenerator = generator();
console.log(runGenerator.next(3)); // Returns { value: 1, done: false }
console.log(runGenerator.next(3)); // Returns { value: 4, done: false }
console.log(runGenerator.next(3)); // Returns { value: 4, done: false }
console.log(runGenerator.next(3)); // Returns { value: 4, done: false }

Interestingly, next() only passes this value to yield after the first run. So in the first run, we get the value current + 1. After that, the yield current is replaced by 3 – so every value after is equivalent to 4. This is quite useful for selecting specific items in an iteration.

Consider Another Example

function* generator() {
    yield yield yield 5 * 2
}

let runGenerator = generator();
console.log(runGenerator.next(3)); // Returns { value: 10, done: false }
console.log(runGenerator.next(3)); // Returns { value: 3, done: false }
console.log(runGenerator.next(3)); // Returns { value: 3, done: false }
console.log(runGenerator.next(3)); // Returns { value: undefined, done: false }

In this example, the first number runs fine, as before. Then after yield 5 * 2 is replaced by our next() value, 3, meaning yield yield yield 5 * 2 becomes yield yield 3.

After that, we replace it again, so yield yield 3 becomes yield 3.

Finally, we replace it again – yield 3 becomes 3. Since we have no more yields left

Generators Are Iterable

Generators differ from normal functions and objects in that they are iterable. That means they can be used with for(... of ...), allowing us to iterate over them and further control when and where we stop using them. For example, to iterate over each item in an iterator, and return only values, we can do this:

For example:

function* generator() {
    let current = 0;
    while(current < 5) {
        let previous = current;
        current = current + 1;
        yield [ current, previous ]
    }
}

for(const i of generator()) {
    console.log(i);
}
// console logs: 
// [ 1, 0 ]
// [ 2, 1 ]
// [ 3, 2 ]
// [ 4, 3 ]
// [ 5, 4 ]

Example: Defining an Infinite Data Structure

Since generators only run when we call them, we can define a function that returns numbers up to infinity, but will only generate one when it is called. You can easily see how this could be useful for defining unique user IDs:

function* generator() {
    let current = 0;
    while(true) {
        yield ++current;
    }
}

let runGenerator = generator();
console.log(runGenerator.next()); // Returns { value: 1, done: false }
console.log(runGenerator.next()); // Returns { value: 2, done: false }
console.log(runGenerator.next()); // Returns { value: 3, done: false }
console.log(runGenerator.next()); // Returns { value: 4, done: false }

Conclusion

Generator functions provide a great, memory-efficient way to iterate through items, whether that be in a calculation, or from an API. With generators, you can make memory-efficient functions that can be incredibly useful in complex applications.

.

Leave a Comment