JavaScript Generators Tutorial
By Flavio
Tagged: JavaScript
Learn JavaScript generators: how yield pauses a function, how for...of consumes one, and when a generator beats building a full array.
Generators are a special kind of function with the ability to pause itself, and resume later, allowing other code to run in the meantime.
The code decides that it has to wait, so it lets other code “in the queue” to run, and keeps the right to resume its operations “when the thing it’s waiting for” is done.
All this is done with a single, simple keyword: yield. When a generator contains that keyword, the execution is halted.
A generator can contain many yield keywords, thus halting itself multiple times. The syntax is function*. A method uses *name() {}. That * sits after function (or before the method name). It is not the pointer operator from C.
There are also async generators, async function*, which you iterate with for await...of. They have been in the language since ES2018. See async/await if you need a refresher.
Generators enable whole new paradigms of programming in JavaScript, allowing:
- 2-way communication while a generator is running
- long-lived while loops which do not freeze your program
A simple generator
Calling a generator function does not run the body. It returns a generator object. That object is an iterator. You step it with next().
Each next() run goes until the next yield, then pauses. The value you yield is what next() returns.
function* colors() {
yield 'red'
yield 'green'
yield 'blue'
}
const list = colors()
list.next() //{ value: 'red', done: false }
list.next() //{ value: 'green', done: false }
list.next() //{ value: 'blue', done: false }
list.next() //{ value: undefined, done: true }
done: true means there is nothing left to yield. Another next() after that keeps returning { value: undefined, done: true }.
The usual way to consume a generator is for...of. It calls next() for you and stops when done is true:
for (const color of colors()) {
console.log(color)
}
//red
//green
//blue
A return inside the generator also finishes it. for...of ignores the returned value. next() still gives it to you as value with done: true.
Sending values back in
next() can also send a value into the generator. That value becomes the result of the yield that paused last time. The first next() starts the function, so the value you pass there is ignored.
Here is an example of a generator which explains how it all works.
function *calculator(input) {
var doubleThat = 2 * (yield (input / 2))
var another = yield (doubleThat)
return (input * doubleThat * another)
}
We initialize it with
const calc = calculator(10)
Then we start the iterator on our generator:
calc.next()
This first iteration starts the iterator. The code returns this object:
{
done: false
value: 5
}
What happens is: the code runs the function, with input = 10 as it was passed in the generator constructor. It runs until it reaches the yield, and returns the content of yield: input / 2 = 5. So we got a value of 5, and the indication that the iteration is not done (the function is just paused).
In the second iteration we pass the value 7:
calc.next(7)
and what we got back is:
{
done: false
value: 14
}
7 was placed as the value of doubleThat. Important: you might read like input / 2 was the argument, but that’s just the return value of the first iteration. We now skip that, and use the new input value, 7, and multiply it by 2.
We then reach the second yield, and that returns doubleThat, so the returned value is 14.
In the next, and last, iteration, we pass in 100
calc.next(100)
and in return we got
{
done: true
value: 14000
}
As the iteration is done (no more yield keywords found) and we just return (input * doubleThat * another) which amounts to 10 * 14 * 100.
Things that do not work
There is no arrow-function form. const colors = *() => {} is a SyntaxError. Use function* or *name() {} on an object or class.
yield only works in the generator body. You cannot put it inside a nested callback, a map() function, or a regular inner function. Those are not the generator.
Calling colors twice gives you two separate generators. Each one has its own place in the function.
When should you use a generator?
Use a generator when you want values one at a time, and you do not want to build the whole list first.
A long or infinite sequence is the clear case. You yield the next number when someone asks. You never keep a million items in an array.
Custom iterators are the other case. Put a *[Symbol.iterator]() method on an object, and for...of and Array.from() can walk it.
Do not use a generator to wait for a network request. That used to be a pattern. async/await is the way to do that now. If you need to yield values that arrive over time, use async function* and for await...of.
Want me to talk about your product? You can sponsor this site.
Related posts about js: