Click to share! ⬇️

Generators are a special type of function in JavaScript that allow you to pause and resume the execution of a function. This means that you can write functions that can yield multiple values, and then pause the function in the middle of execution before continuing later on. One of the main benefits of generators is that they allow you to write more flexible and efficient code by avoiding the need to generate all the output at once. Instead, you can generate values as needed, which can be especially useful when dealing with large datasets or other scenarios where generating all the output at once would be impractical.

To define a generator function, you use the function* keyword instead of the regular function keyword. Inside the function, you use the yield keyword to pause the function and return a value. When the function is called again, it resumes execution from where it left off, with the internal state of the function preserved.

Here’s a simple example of a generator function that yields the numbers 1, 2, and 3:

function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

We’ll explore this function and more advanced generator concepts in the following sections.

Understanding the Yield Keyword

The yield keyword is the heart of the generator concept. It is used to pause the execution of the generator function and return a value to the caller. When the generator function is resumed, execution continues from where it left off, with the internal state of the function preserved.

Here’s an example of a generator function that uses the yield keyword:

function* myGenerator() {
  yield 'Hello';
  yield 'World';
}

When you call this function, it returns a generator object, which can be used to iterate over the values generated by the function. For example, you can use a for...of loop to iterate over the values:

const gen = myGenerator();

for (const value of gen) {
  console.log(value);
}
// Output:
// Hello
// World

In this example, the generator function generates the strings 'Hello' and 'World' using the yield keyword. When the for...of loop iterates over the generator object, it retrieves each value one at a time and logs it to the console.

Note that the generator function does not execute until it is called. When you call the function, it returns a generator object, but does not start executing the function until you start iterating over the values using the next() method of the generator object.

const gen = myGenerator();
console.log(gen.next()); // { value: 'Hello', done: false }
console.log(gen.next()); // { value: 'World', done: false }
console.log(gen.next()); // { value: undefined, done: true }

In this example, we call the next() method on the generator object to retrieve the next value. The next() method returns an object with two properties: value, which contains the value generated by the yield keyword, and done, which is a boolean value indicating whether the generator function has finished executing.

Creating a Simple Generator Function

To create a simple generator function, you use the function* syntax and the yield keyword to generate values one at a time. Here’s an example of a simple generator function that generates the numbers from 1 to 3:

function* generateNumbers() {
  yield 1;
  yield 2;
  yield 3;
}

When you call this function, it returns a generator object, which can be used to retrieve the values generated by the function:

const gen = generateNumbers();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

In this example, we create a generator object by calling the generateNumbers() function. We then use the next() method of the generator object to retrieve each value generated by the function.

Note that when the generator function is called, it does not execute immediately. Instead, it returns a generator object that can be used to control the execution of the function. When the next() method is called on the generator object, the function executes until it reaches the next yield statement, at which point it pauses and returns the value specified by the yield keyword.

You can also use parameters to control the behavior of the generator function. For example, you could create a generator function that generates a sequence of Fibonacci numbers:

function* fibonacci(limit) {
  let current = 0;
  let next = 1;
  
  while (current < limit) {
    yield current;
    [current, next] = [next, current + next];
  }
}

In this example, the generator function takes a limit parameter, which specifies the maximum value that should be generated. The function then generates the sequence of Fibonacci numbers using the yield keyword, until it reaches the limit.

Iterating over Generator Output with for…of Loops

One of the simplest ways to iterate over the output of a generator function is to use a for...of loop. The for...of loop can be used to iterate over any iterable object, including generator objects.

Here’s an example of a generator function that generates a sequence of even numbers:

function* generateEvenNumbers(max) {
  for (let i = 0; i < max; i += 2) {
    yield i;
  }
}

To iterate over the values generated by this function using a for...of loop, you would do the following:

for (const value of generateEvenNumbers(10)) {
  console.log(value);
}
// Output:
// 0
// 2
// 4
// 6
// 8

In this example, we create a for...of loop that iterates over the values generated by the generateEvenNumbers() function. The loop iterates over the even numbers between 0 and 10, which are generated by the function using the yield keyword.

Note that the for...of loop automatically stops iterating when the generator function finishes generating values. In this example, the loop stops after generating the first five even numbers between 0 and 10.

You can also use the next() method of the generator object to manually control the iteration over the generator function. Here’s an example:

const gen = generateEvenNumbers(10);

while (true) {
  const { value, done } = gen.next();

  if (done) {
    break;
  }

  console.log(value);
}
// Output:
// 0
// 2
// 4
// 6
// 8

In this example, we create a generator object by calling the generateEvenNumbers() function. We then use a while loop to manually iterate over the values generated by the function, using the next() method of the generator object to retrieve each value one at a time. The loop continues until the generator function is finished generating values.

Pausing and Resuming Generator Execution

One of the key features of generator functions is the ability to pause and resume the execution of the function. This allows you to generate values on-the-fly, without having to generate all of the values at once.

To pause the execution of a generator function, you use the yield keyword. When the function is paused, the current state of the function is saved, and the generator object is returned to the caller.

Here’s an example of a generator function that generates a sequence of numbers:

function* generateNumbers(max) {
  for (let i = 0; i < max; i++) {
    yield i;
  }
}

To pause the execution of this function, you would use the yield keyword, like this:

function* generateNumbers(max) {
  for (let i = 0; i < max; i++) {
    yield i;
  }
}

const gen = generateNumbers(5);
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }

// Pause execution for 1 second
setTimeout(() => {
  console.log(gen.next()); // { value: 3, done: false }
}, 1000);

console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: undefined, done: true }

In this example, we create a generator object by calling the generateNumbers() function. We then use the next() method of the generator object to retrieve the first three values generated by the function. We then pause the execution of the function for 1 second using the setTimeout() function, before retrieving the next value generated by the function.

When the setTimeout() function completes, the generator function resumes execution from where it left off, and generates the value 3. The function then generates the final value, 4, before finishing execution.

Note that when a generator function finishes executing, it returns a value of undefined and sets the done property of the generator object to true.

Passing Values into and out of Generators

Generator functions can also receive values from the caller, and return values back to the caller. This allows you to create more flexible and dynamic generator functions.

To pass a value into a generator function, you can use the next() method of the generator object to send a value back to the generator. The value that is passed to the next() method becomes the result of the most recent yield expression.

Here’s an example of a generator function that generates a sequence of numbers, with the ability to modify the sequence using a yield expression:

function* generateNumbers() {
  let i = 0;

  while (true) {
    const modifier = yield i;
    i += modifier || 1;
  }
}

In this example, the generator function starts with the number 0, and generates a sequence of numbers by incrementing the current value by a modifier. The modifier is specified by the yield expression, and is passed to the function by the caller using the next() method of the generator object.

Here’s an example of how to use this generator function:

const gen = generateNumbers();

console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next(2)); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next(-1)); // { value: 2, done: false }

In this example, we create a generator object by calling the generateNumbers() function. We then use the next() method of the generator object to retrieve the first value generated by the function. We then modify the sequence by passing a value of 2 to the generator using the next() method, which causes the next value generated to be 2. We then retrieve the next value generated by the function, which is 3. Finally, we modify the sequence again by passing a value of -1 to the generator using the next() method, which causes the next value generated to be 2.

Using Generators for Asynchronous Operations

Generator functions can also be used to simplify asynchronous code by allowing you to write asynchronous operations in a synchronous style. This is achieved by using the yield keyword to pause the generator function until an asynchronous operation has completed.

Here’s an example of a generator function that uses the yield keyword to wait for a promise to complete before generating a value:

function* generateValues() {
  const value1 = yield new Promise((resolve) => setTimeout(() => resolve(1), 1000));
  const value2 = yield new Promise((resolve) => setTimeout(() => resolve(2), 1000));
  const value3 = yield new Promise((resolve) => setTimeout(() => resolve(3), 1000));

  return [value1, value2, value3];
}

In this example, the generator function waits for three promises to complete before returning an array of values. Each promise is wrapped in a yield expression, which causes the generator function to pause until the promise is resolved.

Here’s an example of how to use this generator function:

const gen = generateValues();

gen.next().value.then((result1) => {
  console.log(result1); // Output: 1

  gen.next(result1).value.then((result2) => {
    console.log(result2); // Output: 2

    gen.next(result2).value.then((result3) => {
      console.log(result3); // Output: 3

      const finalResult = gen.next(result3).value;
      console.log(finalResult); // Output: [1, 2, 3]
    });
  });
});

In this example, we create a generator object by calling the generateValues() function. We then use the next() method of the generator object to retrieve the first promise generated by the function. We then use the then() method of the promise to retrieve the result of the promise, which is the value 1. We then pass this value back to the generator function using the next() method of the generator object, which causes the generator function to continue executing and generate the next promise.

We repeat this process for the next two promises, passing the result of each promise back to the generator function using the next() method of the generator object. Once all three promises have completed, the generator function returns an array of the results.

Combining Generators with Promises

Generator functions can also be combined with promises to create more flexible and powerful asynchronous code. Promises can be used to represent the results of asynchronous operations, and generators can be used to control the flow of execution.

Here’s an example of a generator function that generates a sequence of values using promises:

function* generateValues() {
  const value1 = yield new Promise((resolve) => setTimeout(() => resolve(1), 1000));
  const value2 = yield new Promise((resolve) => setTimeout(() => resolve(2), 1000));
  const value3 = yield new Promise((resolve) => setTimeout(() => resolve(3), 1000));

  return [value1, value2, value3];
}

In this example, the generator function waits for three promises to complete before returning an array of values. Each promise is wrapped in a yield expression, which causes the generator function to pause until the promise is resolved.

To simplify the process of retrieving the values generated by this function, you can use a utility function that recursively calls the next() method of the generator object until the generator function has finished executing:

function runGenerator(generator) {
  const iterator = generator();

  function iterate(iteration) {
    if (iteration.done) {
      return iteration.value;
    }

    return Promise.resolve(iteration.value).then((result) => iterate(iterator.next(result)));
  }

  return iterate(iterator.next());
}

This utility function takes a generator function as an argument, creates a generator object using the next() method of the generator function, and then iterates over the values generated by the function using a recursive function.

Here’s an example of how to use this utility function:

const result = runGenerator(generateValues);
result.then((values) => {
  console.log(values); // Output: [1, 2, 3]
});

In this example, we call the runGenerator() function with the generateValues function as an argument. The runGenerator() function iterates over the values generated by the generateValues() function, and returns a promise that resolves to an array of the results.

Common Use Cases for Generators in JavaScript

Generators can be used in many different ways to simplify and improve your JavaScript code. Here are some common use cases for generators in JavaScript:

  1. Lazy Evaluation: Generators can be used to generate values on-the-fly, which can be useful for generating large sequences of values that may not be needed immediately. This can help reduce memory usage and improve performance, especially for long sequences of data.
  2. Asynchronous Programming: Generators can be used to simplify asynchronous programming, by allowing you to write asynchronous code in a synchronous style. By using the yield keyword to pause execution until an asynchronous operation has completed, you can write asynchronous code that is more readable and maintainable.
  3. Stateful Iteration: Generators can be used to maintain state between iterations, which can be useful for stateful iteration over large datasets. By using the yield keyword to pause execution and return a value, you can maintain state between iterations, and generate values on-the-fly based on the current state.
  4. Infinite Sequences: Generators can be used to generate infinite sequences of values, which can be useful for generating test data or simulating real-world scenarios. By using the yield keyword to generate values on-the-fly, you can create infinite sequences of data that can be used in a variety of applications.
  5. Parsing: Generators can be used to parse complex data structures, such as JSON or XML. By using the yield keyword to parse data one element at a time, you can simplify the process of parsing complex data structures, and improve the performance of your code.

Generators are a flexible tool for simplifying and improving your JavaScript code. Whether you’re working with large datasets, asynchronous operations, or complex data structures, generators can help you write more readable, maintainable, and performant code.

Click to share! ⬇️