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:

  1. What are generator functions and generator objects
  2. How to use the yield and yield* operators
  3. How to iterate over generators with for...of and other methods
  4. What are the benefits and use cases of generators
  5. 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:

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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.

Javascript
                        
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:

  1. 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.
  2. Use descriptive names for the generator functions and the generator objects. This helps to avoid confusion and improve readability.
  3. Use the yield keyword sparingly and only when necessary. Too many yield statements can make the code hard to follow and debug.
  4. Use the yield* operator to delegate the execution to another generator or iterable object. This helps to avoid code duplication and improve modularity.
  5. Use the for...of loop or other iteration methods to iterate over generators. This makes the code more concise and expressive.
  6. 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.