In JavaScript, a generator function is a special type of function that can pause its execution and then resume it at a later time.
Generator functions are useful when we need to work with a large amount of data or with large sequences, as they only compute values as we request them (instead of calculating all values at once).
Basic Syntax of Generator Functions
Generator functions are defined using the
function*
syntax (the asterisk indicates that the function is a generator).On the other hand, inside the function we will use the
yield
keyword (which pauses execution and returns a value)
How yield
Works
- Every time it encounters a
yield
, the function returns the result and suspends at that point. - When
next()
is called again, the generator function continues executing right after theyield
that suspended it. - It will execute until it finds another
yield
or finishes execution.
The result returned by yield
is an object with the properties value
(the generated value) and done
(a boolean indicating whether the sequence has ended).
Basic Example
Let’s see it with an example. First, we will define a very simple generator function that returns a sequence 1, 2, and 3.
function* exampleGenerator() {
yield 1; // Pause here and return 1
yield 2; // Pause here and return 2
yield 3; // Pause here and return 3
}
In this example,
- The function
exampleGenerator
generates three sequential values: 1, 2, and 3. - Each
yield
will return a value. - Execution will remain suspended until it is called again.
Now let’s see how we can use our generator function. To do this, we simply need to invoke its next()
method to request the next value.
const generator = exampleGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
In this example,
- The
next()
function is called repeatedly to get the next value. - The obtained result contains
value
anddone
.
Generator Functions and Iterators
The relationship between generator functions and iterators is very close (in fact, they are designed to work together).
An iterator is an object that must have a next()
method, which returns an object with the properties value
and done
.
As we have seen, this is exactly the behavior of generator functions when we use yield
.
That is, any generator function is also an iterator. In fact, we can use them in a for...of
loop.
function* colors() {
yield 'red';
yield 'green';
yield 'blue';
}
for (const color of colors()) {
console.log(color); // 'red', 'green', 'blue'
}
In this example,
- The
for...of
loop automatically callsnext()
on the generator and handles the return value of each iteration. - When the generator function finishes producing values, the
for...of
loop automatically ends.
If you want to learn more about iterables and iterators, check out the entry
Practical Examples
Fibonacci Sequence
Let’s look at a practical example of a generator that produces the Fibonacci sequence:
function* fibonacciGenerator() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const iterator = fibonacciGenerator();
console.log(iterator.next().value); // 0
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().value); // 5
// And so on...
In this example, the generator fibonacciGenerator
produces the Fibonacci sequence efficiently without needing to store all values in memory.
Infinite Generators
An interesting use case is to generate infinite sequences (logically, this would be impossible with normal arrays because we would run out of memory).
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const generator = infiniteSequence();
console.log(generator.next().value); // 0
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
In this example,
- The generator function
infiniteSequence
generates consecutive numbers infinitely, incrementingi
each timenext()
is called. - Since this is an infinite sequence, it has no end, and the code will continue generating numbers until explicitly interrupted.
Generators with Parameters
Like regular functions, generator functions can receive parameters. This can be useful if you want to customize the sequence of values they generate.
function* countTo(max) {
for (let i = 0; i <= max; i++) {
yield i;
}
}
const counter = countTo(5);
console.log([...counter]); // [0, 1, 2, 3, 4, 5]
In this case, the generator function receives a parameter max
and generates values from 0 to the provided maximum value.
Asynchronous Generators
One of the advanced features of generator functions is that they can be used with promises to handle asynchronous flows.
Asynchronous generator functions are defined with the async
keyword and are used along with await
to work with asynchronous operations.
async function* asyncGenerator() {
const data = await fetchData();
yield data;
}
async function fetchData() {
return 'Data loaded';
}
const gen = asyncGenerator();
gen.next().then(result => console.log(result.value)); // 'Data loaded'
Bidirectional Communication with yield
We can send data to a generator function using the next()
method. This allows for bidirectional communication between the generator function and the code that uses it.
function* interactiveGenerator() {
const name = yield "What is your name?";
yield `Hello, ${name}!`;
}
const gen = interactiveGenerator();
console.log(gen.next().value); // "What is your name?"
console.log(gen.next("Juan").value); // "Hello, Juan!"
Delegating Generators
We can delegate part of a generator function’s work to another using yield*
.
function* generatorA() {
yield 1;
yield 2;
}
function* generatorB() {
yield* generatorA();
yield 3;
}
for (const value of generatorB()) {
console.log(value);
}
// Output:
// 1
// 2
// 3