Generators are functions which can stop halfway through execution, and then continue from where it 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 function, and when we ran our function, we used 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 a 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, 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 which 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 calculation, or from an API. With generators, you can make memory efficient functions that can be incredibly useful in complex applications. I hope you've enjoyed this article - you can find more Javascript content here.
Top comments (2)
This is a very good article!
thanks man that means a lot!