Everything you need to know about Generators in JavaScript πŸš€

Everything you need to know about Generators in JavaScript πŸš€

A simple & beginner-friendly guide to using JavaScript generators

Β·

10 min read

If you have been working with JavaScript quite a lot, you might have come across some syntax like this:

function* makeIterator() {
    yield 1;
    yield 2;
}

Now, you might be wondering what is this code exactly doing? Well, honestly speaking, I felt the same when I came across such syntax for the first time! πŸ˜…

I don't understand

This weird yet quite useful piece of code is what we call Generators in JavaScript.

Let's try and understand what are Generators, how to use them, and when to use them in your JavaScript code.

Demystifying Iterators & Generators

Now, before we try to understand generators, let's first understand the concept of Iterables and Iterators so that we can understand generators better.

Iterables: Iterables are basically data structures that have the Symbol.iterator() method or the Symbol.iterator key. Some of the most commonly used data structures in JavaScript like Array, String, Set, and Map are examples of Iterables.

The Symbol.iterator() method defines how these iterables can be iterated by using iterators and the for...of loop.

Iterators: An iterator is an object that is returned by the Symbol.iterator() method. This object has a next() method that returns an object with two properties:

value: The value of the next element in the iteration sequence or the iterable.
done: This is a boolean value. It is set to true if the last element in the iteration sequence is already iterated else it is set to false.

Let's see a simple example of how we can iterate through all the elements in the Array using a for...of loop and an Array Iterator.

const number = [ 1, 2, 3 ];
const arrayIterator = number[Symbol.iterator]();

console.log(arrayIterator);

// Iterating through the iterator to access array elements
for (let n of arrayIterator) {
    console.log(n);
}

Output:

Array Iterator {}
1
2
3

So, what are generators?

A generator is a process that can be paused and resumed and can yield multiple values. A generator in JavaScript consists of a Generator function, which returns an iterable Generator object and this object is basically a special type of Iterator.

Now, what are generator functions?

Let's have a look at a simple example of a generator function to understand it better.

// Generator function declaration
function* generatorFunction() {}

As you can see above, a Generator function can be defined by using the function keyword followed by an asterisk (*). Occasionally, you will also see the asterisk next to the function name, as opposed to the function keyword, such as function *generatorFunction(). This works the same, but function* is generally a more widely accepted syntax.

Generator functions can also be defined in regular function expressions:

// Generator function expression
const generatorFunction = function*() {}

Why do we need to use Generators?

We can define our own custom iterators for our specific use cases, so why should we even care about generators at all?

There's an added complexity with custom iterators that you have to carefully program them and also explicitly maintain their internal state to know which elements have been iterated and when to stop the function execution.

Generators provide a powerful alternative to custom iterators that allows you to define your own iteration algorithms using generator functions that do not execute code continuously.

Powerful Generators!

Let's look at how you can use these Generator functions & Generators for your specific use cases.

Working with Generator functions & Generators

A generator function, when invoked, returns an iterable Generator object which is basically nothing but a generator. A generator function differs from a traditional function JavaScript in that generator functions do not return a value immediately instead they return a Generator object which is an iterator as we saw above.

Let's have a look at an example to make this difference between generator functions and traditional functions clear.

In the following code, we create a simple power() function that calculates the power of a given number by using two integer parameters (base no and the exponent no) and returns this calculated value.

// A regular function that calculates power of a no
function power(base, exponent) {
  return Math.pow(base, exponent);
}

Now, calling this function returns the power of the given no,

power(2, 3); // 8

Now, let's create the same function above but as a generator function,

// A generator function that calculates power of a no
function* powerGeneratorFunction(base, exponent) {
  return Math.pow(base, exponent);
}

const powerGenerator = power(2, 3);

Now, when we invoke the generator function, it will return the Generator object which looks something like this,

power {<suspended>}
[[GeneratorLocation]]: VM305:2
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: Ζ’* power(base, exponent)
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]

This Generator object has a next() method similar to Iterators. Let's call this method and see what we get,

// Call the next method on the Generator object
powerGenerator.next();

This will give the following output:

{ value: 8, done: true }

As we saw earlier, a Generator object is nothing but an Iterator, hence the next() method returns something similar to Iterators i.e an object containing two properties, value & done. The power of 2 raised to 3 is 8 which is reflected by value. The value of done is set to true because this value came from a return that closed out this generator.

We saw how to create a simple generator function and how these functions work. Now let's have a look at some of the features of these generators that make them quite useful and unique.

The yield operator

Generators introduce a new keyword to JavaScript: yield. The yield operator can pause a generator function and return the value that follows yield, providing a lightweight way to iterate through values.

Let's have a look at an example to understand this better.

function* simpleGeneratorFunction () {
  console.log("Before 1");
  yield 1;
  console.log("After 1");
  console.log("Before 2");
  yield 2;
  console.log("After 2");
  console.log("Before 3");
  yield 3;
  console.log("After 3");
}

const simpleGenerator = simpleGeneratorFunction();

In the above code snippet, we have defined a simple generator function that contains 3 yield statements. Now, let's call the next() method to see how this yield operator works.

// Call the next() method four times
console.log(simpleGenerator.next());
console.log(simpleGenerator.next());
console.log(simpleGenerator.next());
console.log(simpleGenerator.next());

Now, when we call next() on the generator function, it will pause every time it encounters yield. The done property will be set to false after each yield, indicating that the generator has not finished. Once we encounter a return statement or if there are no more yield statements left, then the done property will be set to true.

Let's look at the output after calling the next() method four times.

Before 1
{value: 1, done: false}
After 1
Before 2
{value: 2, done: false}
After 2
Before 3
{value: 3, done: false}
After 3
{value: undefined, done: true}

Iterating Over a Generator

Using the next() method, we manually iterated through the Generator object, receiving all the value and done properties of the full object. We can iterate over a generator similar to how we can iterate over data structures like Array, Map, or Set using a simple for...of loop.

Let's have a look at the above example and see how we can iterate over generators.

// Creating a new Generator object.
const newGenerator = simpleGeneratorFunction();

// Iterating over the Generator object
for (const value of newGenerator) {
  console.log(value);
}

Note: In the above code snippet, we initialized a new Generator object as we had already manually iterated the older Generator object & hence it can't be iterated again.

This code snippet now returns the following output,

Before 1
1
After 1
Before 2
2
After 2
Before 3
3
After 3

As you can see, we iterated through all the values of the Generator object. Now, let's look at some of the use-cases of Generators and Generator functions.

Passing values in Generators

Generators provide a way to pass custom values through the next() method of the Generator object to modify the internal state of the generator.

Note: A value passed to next() will be received by the yield operator.

Let's take an example to understand this better.

// Take three integers and return their sum
function* sumGeneratorFunction() {
  let sumOfThreeNos = 0;
  console.log(sumOfThreeNos);
  sumOfThreeNos += yield;
  console.log(sumOfThreeNos);
  sumOfThreeNos += yield;
  console.log(sumOfThreeNos);
  sumOfThreeNos += yield;
  console.log(sumOfThreeNos);

  return sumOfThreeNos;
}

const generator = sumGeneratorFunction();

generator.next();
generator.next(100);
generator.next(200);
generator.next(300);

This will give the following output:

0
100
300
600
{value: 600, done: true}

In the above example, we are calculating the cumulative sum of three nos by passing them one by one by passing the value in the next() method.

Note: Passing a value to the first next() method has no effect on the internal state of the generator. Values can be passed from the 2nd call to all the other subsequent calls to this method.

Uses of Generators and Generator functions

Generators and Generator functions are quite powerful and have some really good use-cases.

Power of Generators

  • Generators provide a great way of making iterators and are capable of dealing with infinite data streams, which can be used to implement infinite scroll on the Front-End of a web application.

  • Generators when used with Promise can be used to simulate async/await functionality in JS to allow handling more advanced use-cases while dealing with APIs or asynchronous code. It allows us to work with asynchronous code in a simpler and more readable manner.

  • Generators are also internally used in some of the NPM libraries to provide a custom way for the developer to iterate through some of the objects/data structures implemented by the library. They are also used for internal asynchronous operations to handle more advanced use cases.

Let's discuss one of the major use-cases of Generators that you will most likely come across i.e dealing with infinite iterations.

To demonstrate infinite streams, we consider a simple square number series in Mathematics where each term in the series can be calculated by a simple formula: n^2. Let's create a generator function for this series by creating an infinite loop as follows:

// Create a square number series generator function
function* squareNumberSeries() {
  let n = 1;

  // Square the no and increment it to yield the infinite series
  while (true) {
    const squaredNo = Math.pow(n, 2);
    yield squaredNo;
    n += 1;
  }
}

To test this out, we can loop through a finite number and print the Square number sequence to the console.

// Print the first 10 values of the square number series
const squareNoGenerator = squareNumberSeries();

for (let i = 0; i < 10; i++) {
  console.log(squareNoGenerator.next().value);
}

This will generate the following series:

1
4
9
16
25
36
49
64
81
100

The squareNumberSeries generator function in the above code snippet returns successive values in the infinite loop while the done property remains false, ensuring that it will not finish. With generators, we don’t need to worry about creating an infinite loop, because we can halt and resume its execution at will.

However, we still have to take care with how we invoke the generator.

Using the spread operator or for...of loop on an infinite data stream will cause it to keep iterating over an infinite loop all at once, which will cause the environment to crash.

Working with infinite data streams is one of the most powerful features that generators provide without worrying about managing all the internal states for a custom iterator implementation.

Conclusion

They are a powerful, versatile feature of JavaScript, although they are not commonly used. In this article, we got a brief overview of what are iterators and generators, what are generator functions, why we need to use generators, and finally how to work with generators and generator functions. We also discussed a very powerful use case of generators for dealing with infinite data streams.

That's it from me folks, thank you so much for reading this blog! πŸ™Œ I hope I was able to make generators interesting for you and was able to give a brief overview of them through this blog πŸ˜„

Last but not the least, You folks are awesome!

Feel free to reach out to me:
Twitter
Linkedin
GitHub
You can also reach out to me over mail: devansuyadav@gmail.com

Β