JavaScript Generators: A Complete Guide
JavaScript generators are special functions that can pause and resume their execution, allowing you to produce multiple values on demand. They are useful for simplifying complex asynchronous tasks and creating custom iterators.
In this article, you will learn:
- What are generator functions and generator objects
- How to use the
yield
andyield*
operators - How to iterate over generators with
for...of
and other methods - What are the benefits and use cases of generators
- What are the best practices and tips for working with generators
Generator Functions
To create a generator, you need to use the function*
syntax, which indicates that it is a generator function. For example:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
When you call a generator function, it does not run its code immediately. Instead, it returns a generator object, which has a next()
method to control the execution.
let generator = generateSequence(); // returns a generator object
console.log(generator); // [object Generator]
Yielding Values
The yield
keyword is used to pause the execution of the generator function and return a value to the caller. The value can be any JavaScript expression, or omitted to yield undefined
.
function* generateSequence() {
yield 1; // returns {value: 1, done: false}
yield 2; // returns {value: 2, done: false}
return 3; // returns {value: 3, done: true}
}
The next()
method of the generator object resumes the execution until the next yield
statement or the end of the function. It returns an object with two properties: value
and done
. The value
property is the yielded value, and the done
property is a boolean that indicates whether the generator function has finished or not.
let generator = generateSequence();
let one = generator.next(); // resumes the execution from the start
console.log(one); // {value: 1, done: false}
let two = generator.next(); // resumes the execution from the first yield
console.log(two); // {value: 2, done: false}
let three = generator.next(); // resumes the execution from the second yield
console.log(three); // {value: 3, done: true}
Iterating over Generators
Generators are iterable, which means you can use them with the for...of
loop or other iteration methods. However, the for...of
loop ignores the final value when done
is true
, so you need to handle it separately if you need it.
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for (let value of generator) {
console.log(value); // 1, then 2
}
let final = generator.next(); // get the final value
console.log(final); // {value: 3, done: true}
Delegating to Another Generator
The yield*
operator is used to delegate the execution to another generator or iterable object. This way, you can compose multiple generators into a single one.
function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
function* generateAlphabet() {
yield* generateNumbers(65, 90); // ASCII codes for A-Z
yield* generateNumbers(97, 122); // ASCII codes for a-z
}
let generator = generateAlphabet();
for (let code of generator) {
console.log(String.fromCharCode(code)); // A, then B, ..., then Z, then a, ..., then z
}
Benefits and Use Cases of Generators
One of the benefits of generators is that they enable lazy evaluation, which means they only produce values when requested, and not in advance. This allows you to create sequences of unlimited size, such as the range of integers from 0 to infinity.
function* generateNumbers(start = 0, step = 1) {
let index = start;
while (true) {
yield index;
index += step;
}
}
let generator = generateNumbers(); // creates an infinite sequence
console.log(generator.next().value); // 0
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
// and so on
Another benefit of generators is that they allow you to create custom iterators, which define how to iterate over a certain data structure. For example, you can create a generator that iterates over the keys and values of an object.
function* generateEntries(obj) {
for (let key in obj) {
yield [key, obj[key]];
}
}
let user = {
name: "Alice",
age: 25,
occupation: "Developer",
};
let generator = generateEntries(user); // creates a custom iterator
for (let [key, value] of generator) {
console.log(`${key}: ${value}`); // name: Alice, then age: 25, then occupation: Developer
}
A third benefit of generators is that they can simplify complex asynchronous tasks, such as fetching data from an API, by using the yield
keyword to wait for the results. This can make the code more readable and avoid callback hell or promise chains.
function* fetchUser(id) {
let response = yield fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
let user = yield response.json();
return user;
}
let generator = fetchUser(1);
generator.next().value // returns a promise for the fetch request
.then(response => generator.next(response).value) // returns a promise for the json parsing
.then(user => generator.next(user).value) // returns the final user object
.then(console.log); // {id: 1, name: "Leanne Graham", ...}
Best Practices and Tips for Working with Generators
Here are some best practices and tips for working with generators:
- Use the
function*
syntax to indicate that a function is a generator. This makes it easier to distinguish from regular functions and shows the intention of the function. - Use descriptive names for the generator functions and the generator objects. This helps to avoid confusion and improve readability.
- Use the
yield
keyword sparingly and only when necessary. Too manyyield
statements can make the code hard to follow and debug. - Use the
yield*
operator to delegate the execution to another generator or iterable object. This helps to avoid code duplication and improve modularity. - Use the
for...of
loop or other iteration methods to iterate over generators. This makes the code more concise and expressive. - Use generators to implement lazy evaluation, custom iterators, and complex asynchronous tasks. These are some of the common and useful use cases for generators.
Conclusion
In this article, you learned about JavaScript generators, which are special functions that can pause and resume their execution, allowing you to produce multiple values on demand. You learned how to create and use generator functions and generator objects, how to use the yield
and yield*
operators, how to iterate over generators with for...of
and other methods, what are the benefits and use cases of generators, and what are the best practices and tips for working with generators.
Generators are a powerful and versatile feature of JavaScript that can help you simplify complex asynchronous tasks and create custom iterators. They can also enable lazy evaluation, which can improve the performance and memory efficiency of your code. Generators are widely supported by modern browsers and Node.js, so you can start using them today.