logo
Published on

Understanding Generators in JavaScript

Authors
  • avatar
    Name
    Nadir Tellai

Understanding Generators in JavaScript

Introduction

Introduced in ECMAScript 2015 (ES6), generators are one of the powerful and underutilized features of javascript. They are used by popular libraries like Redux-Saga and Koa to handle asynchronous operations and complex control flows. In this blog we will learn how generators work and how we can benefit from its power.

Definition

A generator is a special function that can stop midway and later resume its execution from where it stopped. The yield keyword is responsible for pausing the flow of the function.

Syntax

The syntax for creating a generator function is achieved by declaring a function with an asterisk (function*) and using the yield keyword within its body.

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

const generator = numberGenerator(); // Does not execute the function yet
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}
  • Generator Function (function*): A special type of function that returns a Generator object.
  • Generator Object: An iterator returned by a generator function.
  • yield: A keyword used to pause and resume a generator function.

notice how each time we call the next function on the Generator object the Generator Function resumes execution from the last
yield to the next one, and return an object with two properties:

  • value: the yielded value.
  • done: true if the function code has finished, otherwise false.

Generators are Iterable

Generator is a subclass of the Iterator class, which means you can iterate over the yielded values of a generator using a for-of loop.

function* numberGenerator() {
    yield 1;
    yield 2;
    yield 3;
}
let generator = numberGenerator();
for (let value of generator)
console.log(value); // Logs 1, 2, and 3

Generators can also work with the spread operator, which exhausts its values.

console.log([...numberGenerator()]); // [1, 2, 3]

Async Generators

Async generators combine generators with promises, allowing to asynchronously generates a sequence of values. They are defined using async function*.

async function* asyncNumberGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  await new Promise(resolve => setTimeout(resolve, 1000));
  yield Promise.resolve(3);
}
const generator = asyncNumberGenerator()
for await (let value of generator)
  console.log(value);  // 1, then 2, then 3 after a delay of 2 seconds 

Use Cases

1. Generating Fibonacci Numbers

The Fibonacci sequence is a classic example where generators can be a great use due to their ability to handle infinite sequences. Each number in the sequence is the sum of the two preceding ones, starting from 0 and 1.

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    [prev, curr] = [curr, prev + curr];
    yield prev;
  }
}

const fibSequence = fibonacci();
console.log(fibSequence.next().value); // 1
console.log(fibSequence.next().value); // 1
console.log(fibSequence.next().value); // 2
...

2. Async Pagination

Generators can be useful for handling pagination when fetching data from an API. An async generator can yield pages of data one at a time,

async function* asyncPaginatedFetch(url) {
  let currentPage = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`${url}?page=${currentPage}`);
    const data = await response.json();
    yield data.items;

    currentPage++;
    hasMore = data.hasMore;
  }
}

let paginator = asyncPaginatedFetch('https://api.example.com/data')

let page1 =  await paginator.next()
let page2 =  await paginator.next()
let page3 =  await paginator.next()
...

Memory efficient

Generators in JavaScript offer a unique approach to handling data and asynchronous operations in a memory-efficient manner. They generate values on demand, rather than computing and storing an entire series of values upfront. This means at any given moment, only the current value is held in memory. which makes them an efficient solution for handling large sequences of data with minimal memory usage.

Conclusion

While generators are a great tool, they should be used in the correct context otherwise they will add unnecessary complexity for simple use cases.