In JavaScript, an generator function is a special type of function that can pause its execution and later resume it.
Generator functions are useful when we need to work with a large amount of data or large sequences, as they only calculate 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 are going to use the
yieldkeyword (which pauses execution and returns a value)
How yield Works
- Each time it encounters a
yield, the function returns the result and suspends itself at that point. - When
next()is called again, the generator function continues execution right after theyieldthat suspended it. - It will run until it finds another
yieldor finishes execution.
The result returned by yield is an object with the properties value (the generated value) and done (a boolean indicating if the sequence has finished).
Basic Example
Let’s see it with an example. First, let’s define a very simple generator function that returns a sequence of 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
exampleGeneratorfunction generates three sequential values: 1, 2, and 3. - Each
yieldwill return a value - Execution will be suspended until it is invoked again.
Now let’s see how we can use our generator function. To do this, we simply have to call 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
valueanddone
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...ofloop automatically callsnext()on the generator and handles the return value of each iteration. - When the generator function finishes producing values, the
for...ofloop automatically ends.
Practical Examples
Fibonacci Sequence
Let’s see 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 fibonacciGenerator generator produces the Fibonacci sequence efficiently without needing to store all values in memory.
Infinite Generators
An interesting use case is generating 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
infiniteSequencegenerator function generates consecutive numbers infinitely, incrementingieach 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
Just like normal 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 max parameter and generates values from 0 up 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!"
Generator Delegation
We can delegate part of the work of a generator function to another one 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
