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.
- A large data array that would otherwise take up a lot of memory to entirely load in one step.
- Launching tasks to run in parallel with the main program flow.
- Running a task at discretionary times without blocking the main program flow (i.e. asynchronosuly).
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.