DEV Community

Cover image for How to Use Generator and yield in JavaScript
Juan Cruz Martinez
Juan Cruz Martinez

Posted on • Originally published at livecodestream.dev on

How to Use Generator and yield in JavaScript

Some time ago I wrote an article explaining the concept of generators and how to use them in Python, but did you know that JavaScript has its own version of generators? This is actually a concept many people who develop JavaScript apps didn't know it existed, so today we are going to introduce generators in JavaScript.


What are generators?

With ES6 we got introduced to great new functionality like arrow functions, spread operators, and generators among others, but what is a generator? A generator is a function that contrary to normal functions allows for the function to be exited and later re-entered with its context (variable bindings) preserved across re-entrances.

Let's break that down and look into generators step by step so that we can all understand how they work. When we execute a regular function, the interpreter will run all the code into that function until the function is completed (or throws an error). This is known as the run-to-completion model.

Let's take an example of a very simple function:

function regularFunction() {
    console.log("I'm a regular function")
    console.log("Surprise surprice")
    console.log("This is the end")
}

regularFunction()

-----------------
Output
-----------------
I'm a regular function
Surprise surprice
This is the end
Enter fullscreen mode Exit fullscreen mode

Nothing fancy yet, just as you expected is a regular function that's executing until it reaches the end or returns a value. But what if we just want to stop the function at any point to return a value, and then continue? That's when generators enter the picture.

My first generator function

function* generatorFunction() {
    yield "This is the first return"
    console.log("First log!")
    yield "This is the second return"
    console.log("Second log!")
    return "Done!"
}
Enter fullscreen mode Exit fullscreen mode

Before we execute that function you may be wondering about a few things, first what is function*? That's the syntax we use to declare a function as a generator. And what about yield? The yield, in difference to a return, will pause the function by saving all its states and will later continue from that point on successive calls. In both cases, the expression will be returned to the callers' execution.

What did exactly happened to our function as such? Let's find out by calling the function:

generatorFunction()

-----------------
Output
-----------------
generatorFunction {<suspended>} {
    __proto__: Generator
    [[GeneratorLocation]]: VM272:1
    [[GeneratorStatus]]: "suspended"
    [[GeneratorFunction]]: ƒ* generatorFunction()
    [[GeneratorReceiver]]: Window
    [[Scopes]]: Scopes[3]
}
Enter fullscreen mode Exit fullscreen mode

Wait, what? When we call a generator function the function is not automatically triggered and instead, it returns an iterator object. What's particular about this object is that when the method next() is called, the generator function's body is executed until the first yield or return expression. Let's see it in action:

const myGenerator = generatorFunction()
myGenerator.next()

-----------------
Output
-----------------
{value: "This is the first return", done: false}
Enter fullscreen mode Exit fullscreen mode

As explained the generator run until the first yield statement and yielded an object containing a value property, and a done property.

{ value: ..., done: ... }
Enter fullscreen mode Exit fullscreen mode
  • 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)

Let's invoke next() one more time and see what we get

myGenerator.next()

-----------------
Output
-----------------
First log!
{value: "This is the second return", done: false}
Enter fullscreen mode Exit fullscreen mode

This time we first see the console.log in our generator body being executed and printing First log!, and the second yielded object. And we could continue doing this like:

myGenerator.next()

-----------------
Output
-----------------
Second log!
{value: "Done!", done: true}
Enter fullscreen mode Exit fullscreen mode

Now the second console.log statement is executed and we get a new returned object, but this time the property done is set to true.

The value of the done property is not just a flag, it is a very important flag as we can only iterate a generator object once!. Don't believe me? try calling next() one more time:

myGenerator.next()

-----------------
Output
-----------------
{value: undefined, done: true}
Enter fullscreen mode Exit fullscreen mode

Good it didn't crash, but we only got undefined as the value and the done property remains set to true.

Yielding over iterators

Before we move on to some scenarios, there's one more particularity of the yield operator, which is yield*. Let's explain it by creating a function that allows us to iterate over an array, naively we could think of doing:

function* yieldArray(arr) {
    yield arr
}

const myArrayGenerator1 = yieldArray([1, 2, 3])
myArrayGenerator1.next()

-----------------
Output
-----------------
{value: Array(3), done: false}
Enter fullscreen mode Exit fullscreen mode

But that's not quite what we wanted, we wanted to yield each element in the array, so we could try doing something like:

function* yieldArray(arr) {
    for (element of arr) {
        yield element
    }
}

const myArrayGenerator2 = yieldArray([1, 2, 3])
myArrayGenerator2.next()
myArrayGenerator2.next()
myArrayGenerator2.next()

-----------------
Output
-----------------
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
Enter fullscreen mode Exit fullscreen mode

Now we got the desired result, but can we do better? Yes, we can:

function* yieldArray(arr) {
    yield* arr
}

const myArrayGenerator3 = yieldArray([1, 2, 3])
myArrayGenerator3.next()
myArrayGenerator3.next()
myArrayGenerator3.next()

-----------------
Output
-----------------
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
Enter fullscreen mode Exit fullscreen mode

Awesome, by using yield* expression we can iterate over the operand and yield each value returned by it. This applies to other generators, arrays, strings, any iterable object.

Now that you know all about generators in JavaScript, what are they good for?


Uses of Generators

The great thing about generators is the fact that they are lazy evaluated, meaning that the value that gets returned after invoking the next() method, is only computed after we specifically asked for it. This makes generators a good choice for solving multiple scenarios like the ones presented below.

Generating an infinite sequence

As we saw in the Python article, generators are good for generating infinite sequences, this could be anything from prime numbers to a simple count:

function* infiniteSequence() {
    let num = 0
    while (true) {
        yield num
        num += 1
    }
}

for(i of infiniteSequence()) {
    if (i >= 10) {
        break
    }
    console.log(i)
}

-----------------
Output
-----------------
0
1
2
3
4
5
6
7
8
9
Enter fullscreen mode Exit fullscreen mode

Note that in this case, I'm exiting the loop when i >= 10 otherwise, it would be running forever (or until manually stopped).

Implementing iterables

When you need to implement an iterator, you have to manually create an object with a next() method. Also, you have to manually save the state.

Imagine we want to make an iterable that simply returns I, am, iterable. Without using generators we would have to do something like:

const iterableObj = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step === 1) {
          return { value: 'I', done: false};
        } else if (step === 2) {
          return { value: 'am', done: false};
        } else if (step === 3) {
          return { value: 'iterable.', done: false};
        }
        return { value: '', done: true };
      }
    }
  },
}
for (const val of iterableObj) {
  console.log(val);
}

-----------------
Output
-----------------
I
am
iterable.
Enter fullscreen mode Exit fullscreen mode

With generators this is much simpler:

function* iterableObj() {
    yield 'I'
    yield 'am'
    yield 'iterable.'
}

for (const val of iterableObj()) {
  console.log(val);
}

-----------------
Output
-----------------
I
am
iterable.
Enter fullscreen mode Exit fullscreen mode

Better Async?

Some argue generators can help improve the use of promises and callbacks, though I'd preferably simply use await/async.


Caveats

Not all is shiny when we work with generators. There are some limitations by design, and there are 2 very important considerations:

  • Generator objects are one-time access only. Once exhausted, you can't iterate over it again. To do so, you will have to create a new generator object.
  • Generator objects do not allow random access as possible with for instance, arrays. Since the values are generated one by one, you can't get the value for a specific index, you will have to manually call all the next() functions until you get to the desire position, but then, you cannot access the previously generated elements.

Conclusion

Generator functions are great for optimizing the performance of our applications and also help to simplify the code required to build iterators.

I hope you now have a good understanding of generators in JavaScript and that you can use them on your next project.

Thanks for reading!


If you like the story, please don't forget to subscribe to our free newsletter so we can stay connected: https://livecodestream.dev/subscribe

Top comments (0)