Modern JavaScript: Generators
Welcome to the Generators lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
An earlier lesson showed JavaScript's iterators and the corresponding iteration protocols. Iterators let our custom objects work with for-of loops and destructuring, but at the cost of writing verbose code that can be tricky to follow.
Modern JavaScript also has a feature called "generators", which lets us write iterables without all of the verbosity. Generators use the same iteration protocols that we already saw, but in this case we don't have to implement those protocols manually. We'll look at a working example first, then examine it in detail.
Here's a generator that returns three numbers:
>
function* numbers() {yield 1;yield 2;yield 3;}Result:
And here's a for-of loop over it, where the loop sees each of those three numbers:
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const results = [];for (const n of numbers()) {results.push(n);}results;Result:
Now the details:
- The
*infunction* numbers()marks our function as a generator. - When we call a generator function, we get an iterator.
- Each
yieldin the generator provides one value to the iterator.
- The
Generators follow the iterator protocol that we saw in an earlier lesson. The iterator produces objects that look like
{value, done}. We can see this in action by manually calling.next()on thenumbers()generator.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const iterator = numbers();while (true) {const iteration = iterator.next();console.log(iteration);if (iteration.done) {break;}}console output >
function* loneliestGenerator() {yield 1;}const iterator = loneliestGenerator();iterator.next();Result:
{value: 1, done: false}- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
iterator.next();Result:
{value: undefined, done: true} Generators work as iterables in all of the usual places. We can loop over a generator or destructure it. We can also call
Array.from(...)on a generator to turn it into an array.>
function* loneliestGenerator() {yield 1;}Array.from(loneliestGenerator());Result:
[1]
Each call to the generator gives us a new, separate iterator. Because the iterators are separate, iterating one of them doesn't affect the others.
>
function* numbers() {yield 1;yield 2;yield 3;}const firstIter = numbers();const secondIter = numbers();secondIter.next();secondIter.next();[firstIter.next().value, secondIter.next().value];Result:
[1, 3]
The
yieldstatement is unusual among programming language features. Here's one way to think about it:Each
yieldis likereturn, except that it doesn't actually terminate the function. When weyield, the generator function stops running temporarily. The loop (or whatever else is consuming the iterator) gets the yielded value. When the loop asks for another value by calling.next()on the iterator, our generator function resumes at the point where it left off. It runs until it hits anotheryield(producing another{value: ..., done: false}object) or until the function terminates (producing a{value: undefined, done: true}object).Now we can put all of those details together to see how the whole process works:
- Calling a generator like
numbers()gives us an iterator. - Behind the scenes, our loop,
for (const n of numbers()), automatically calls.next()on the iterator. - The generator function runs until it hits a
yield. - The yielded value shows up as the
nvariable inside the loop body. - Steps 2-4 repeat until the generator function returns, which causes the iterator to return
{value: undefined, done: true}, which in turn causes the loop to end.
- Calling a generator like
Our generator functions above were simple: just one or more
yieldstatements. But generators can contain any code that's allowed in a regular function.>
function* numbers() {let x = 1;yield x;x += 1;yield x;}Array.from(numbers());Result:
[1, 2]
>
function* numbersBelow(maximum) {for (let i=0; i<maximum; i++) {yield i;}}Array.from(numbersBelow(3));Result:
[0, 1, 2]
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
Array.from(numbersBelow(4));Result:
[0, 1, 2, 3]
Here's a code problem:
Write a
numbersInRange(min, max)generator that iterates over all numbers fromminup to, but not including,max.function* numbersInRange(min, max) {for (let i=min; i<max; i++) {yield i;}}const results = [Array.from(numbersInRange(0, 2)),Array.from(numbersInRange(1, 3)),// This checks that you actually wrote a generator. No cheating!numbersInRange.constructor.name,];results;- Goal:
[[0, 1], [1, 2], 'GeneratorFunction']
- Yours:
[[0, 1], [1, 2], 'GeneratorFunction']
In practice, you'll write far more generators than manual iterators. But having seen the iteration protocols will make identifying bugs easier, even if you have to look at the docs for the fine details.