Generators and yield expressions in JS

Functions that operate with iterable objects  

A problem with loops made against large data structures -- hundreds or thousands of items -- is efficiently processing them in memory. If you consider looping over large data structures is often associated with sequential operations (e.g. 1,2,3,4,5,6,7...) an Array or Object data type per se -- even as an iterable -- they represent a weak choice for large data structures. The introduction of the for of statement, iterable and iterator protocols, gave way to generators and yield expression which are designed to tackle this issue.

Generators are iterators embodied as functions. In a standard function you call the function and it runs uninterrupted until it finds return. In a generator function -- which are simply called generators -- you can integrate pauses into a function so that each time it's called, it's a continuation of the previous call. In addition, generators have the ability to generate values on-demand -- which is why they get their name -- so they're much more efficient when it comes to handling large data ranges.

To create a generator you append the * to a function declaration, as illustrated in listing 6-11

Listing 6-11. Generators with the * syntax
function* myGenerator() { 
  yield 2;
  yield 3;
  yield 5;
}

let g = myGenerator();
console.log(g.next().value) // outputs 2
console.log(g.next().value) // outputs 3
console.log(g.next().value) // outputs 5

The myGenerator() generator is unconventional and is intended to illustrate the basic use of yield expressions with the yield keyword. The yield keyword works as a combination of return & stop behavior. Notice the generator is assigned to the g reference, once this is done, you can start stepping through the generator with the iterable next() method -- after all, a generator is an iterator.

On the first next() call, the generator gets to yield 2, where it returns 2 and stops until next() is called again. On the second next() call the generator gets to yield 3, it returns 3 and stops until next() is called again. This process goes on until the last yield is reached and the next() method return { value: undefined, done: true } just like all iterators do.

With this initial overview of generators and the purpose of the yield keyword, let's analyze a few real-life scenarios that can benefit from these techniques.

Listing 6-12 illustrates a more realistic example of JavaScript generators, where the generator is designed to return a sequence of prime numbers with the potential to return an infinite amount of prime numbers with minimal memory consumption (i.e. the prime numbers don't need to be hard-coded, they're generated on-demand).

Listing 6-12. Generators
// Generator to calculate prime numbers
function* primeNumbers() { 
  let n = 2;

  while (true) {
    if (isPrime(n)) yield n;
    n++;
  }

  function isPrime(num) {
    for (let i = 2; i <= Math.sqrt(num); i++) {
      if (num % i === 0) {
        return false;
      }
    }
    return true;
  }
}

// Create generator
let primeGenerator = primeNumbers();
// Advance through generator with next(), print out value
console.log(primeGenerator.next().value);
console.log(primeGenerator.next().value);
console.log(primeGenerator.next().value);
console.log(primeGenerator.next().value);
console.log(primeGenerator.next().value);
// Calls to primerGenerator.next() returns infinite prime numbers

Notice how the primerNumbers generator function in listing 6-12 doesn't declare a hard-coded prime numbers array like the previous examples. Internally, the primerNumbers generator starts with a value of n=2 and increases this value through an endless loop (i.e.while(true)) yielding a result each time n evaluates to a prime number via the logic in isPrime.

Next, you can see the primerNumbers generator is initialized and multiple calls are made to the next() method to advance through the generator/iterator. Finally, value is extracted from each next() method result to output the given prime number in the iteration.

By using this technique, you effectively generate prime numbers as they're needed, instead of declaring and loading them in a single step. This is particularly helpful and more efficient for cases where there's a potential for hundreds or thousands of elements.