What Are JavaScript Generators?

What Are JavaScript Generators?
Photo by Halfcut Pokemon / Unsplash

One of the less-known, but cool JavaScript features are generators. Added in ES6, generators provide a different method for handling loops and asynchronous tasks.

You can think of generators as functions with a pause button.

They yield values on demand, unlike the normal functions that run to completion. This unique ability to suspend and resume makes generators ideal for producing sequences and managing asynchronous flows with grace.

Generators example

A generator function is defined using the function* syntax, and it uses the yield keyword to pause execution.

function* pokemonGenerator() {
  yield "Charmander";
  yield "Charmeleon";
  yield "Charizard";
}

const evolution = pokemonGenerator();

console.log(evolution.next()); 
// { value: "Charmander", done: false }

console.log(evolution.next()); 
// { value: "Charmeleon", done: false }

console.log(evolution.next()); 
// { value: "Charizard", done: false }

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

In the above example, pokemonGenerator is a generator function that yields 3 values. The evolution object is an iterator created by invoking the generator function. Calling evolution.next() returns an object with two properties: 

  • value, the yielded value,
  • done, a boolean indicating whether the generator has finished.

Yield keyword

yield keyword is a two-way street. It pauses the generator and opens a channel for value injection.

function* pokemonEvolution() {
  let pokemon = yield { name: "Charmander", strength: 10 };
  
  pokemon = yield {
    name: "Charmeleon",
    strength: Math.round(pokemon.strength * 1.5)
  };
  
  pokemon = yield {
    name: "Charizard",
    strength: Math.round(pokemon.strength * 2)
  };
}

const evolution = pokemonEvolution();

console.log(evolution.next());
// { value: {name: "Charmander", strength: 10}, done: false }

console.log(evolution.next({ strength: 20 }));
// { value: {name: "Charmeleon", strength: 30}, done: false }

console.log(evolution.next({ strength: 20 }));
// { value: {name: "Charizard", strength: 100}, done: false }

Each evolution.next() awakens the evolution of our beloved dragon, generating new Pokemon forms into evolution. This behavior allows generators to change their internal state between heartbeats.

Here's a breakdown of what's happening:

  1. The first next() call starts the generator and yields Charmander with a default strength of 10.
  2. The second next({ strength: 20 }) passes in a strength of 20 for Charmander. This is used to calculate Charmeleon's strength: 20 * 1.5 = 30.
  3. The third next({ strength: 50 }) passes in a strength of 50 for Charmeleon. This is used to calculate Charizard's strength: 50 * 2 = 100.

When to use generators

You shouldn't force yourself to use generators, except in situations when they are needed. Some of these situations are:

Handling large data sets

function* largeDataSet(n) {
  for (let i = 0; i < n; i++) {
    yield heavyComputation(i);
  }
}

for (const item of largeDataSet(1000000)) {
  // Process each item without loading all into memory
}

Custom iterables

class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end = end;
    this.step = step;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i += this.step) {
      yield i;
    }
  }
}

for (let num of new Range(1, 10, 2)) {
  console.log(num);
}

Lazy evaluation of infinite sequences

function* fibonacciSequence() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacciSequence();
for (let i = 0; i < 10; i++) {
  console.log(fib.next().value);
}

State machines

function* trafficLight() {
  while (true) {
    yield 'green';
    yield 'yellow';
    yield 'red';
  }
}

const light = trafficLight();
console.log(light.next().value); // 'green'
console.log(light.next().value); // 'yellow'

Of course, there are more applications for generators than those above. You can think of it as a Swiss Army knife with unfolded blades.

Conclusion

Generators are JavaScript's elegant dance of sequence and state. You probably won't need them often but there are situations where generators fit perfectly to your solution of a specific problem. In those situations, knowing what will fit perfectly as a final piece in your puzzle is always good.