Everything you need to know about Generators in JavaScript 🚀
Devansu Yadav
Posted on June 26, 2022
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! 😅
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.
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 olderGenerator
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 theyield
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.
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 simulateasync/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 dont 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 😄
Feel free to reach out to me:
GitHub
You can also reach out to me over mail: devansuyadav@gmail.com
Posted on June 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 24, 2024