Generator Function: the powerful JavaScript tool nobody is talking about

Photo by Anna Jiménez Calaf on Unsplash

Hi guys…anyone who has interviewed for a web developer position knows they might find the picky examiner asking for theory over the theory of Javascript.

Usually, in those moments I say to myself: Hey but, do I really need to remember the precise name of a function that I will never use in Javascript just to prepare for this interview?

However, today we are not talking about theory, and also not about anything particularly hidden in the MDN Web Docs.

Today we talk about Generator Functions, a powerful alternative to iterators that provide a set of tools to make our code more performing, more efficient and easier to read.

A Generator Function is a special type of function, introduced with ES6, which operates as a “factory” of iterators.
In fact, they are functions from which it’s possible to exit in order to enter later, keeping the context of the function for when the function will be subsequently called.

Quick example
To give a quick example, think about when you decide to take a break from work in the afternoon and play a game on the PlayStation.
The break (unfortunately) doesn’t last long and so you are forced to save your game and resume the evening after work.
In this way, just like a Generator Function, we performed actions within the game, saved the state of our game, exited the function and then returned to it tonight picking up where we left off.

To declare a Generator Function it is necessary to use the following reserved word function* which returns an object of type Generator.

In the following example, we have a function generator that doesn’t run immediately. In fact, the function returns an iterator object and only starting from the first next() called, the generator body will execute until the first yield expression.

function* increment(i) {
yield i;
console.log("hello");
yield i + 10;
console.log("world!");
yield i;
}
const generator = increment(5);console.log(generator.next().value); // output: 5console.log(generator.next().value);
// output: hello 15
console.log(generator.next().value);
// output: world! 5
console.log(generator.next().value);
// output: undefined

Furthermore, some readers will surely have noticed that there are only 3 yield keywords and the generator function is invoked 4 times.
In fact, once all the values have been “consumed”, to generate the values again it is necessary to create a new generator object.

The Generator Object returned by the function conforms to both the iterable protocol and the iterator protocol.

In fact, the methods of an object generator are:

  • Generator.prototype.next() : returns a value yielded by the yield expression.
    Every invocation of next() returns an object with two properties:
{
value: any,
done: boolean
}

The value property of course contains the value, instead, the done property indicates if the generator has ended the execution and won’t generate any more values.

Remember:
If you want to stop the execution of a generator, you can also return and the done property will become true , blocking any more values generation.

function* increment(i) {
yield i;
yield i + 10;
return 'stop'; // Generator ends here
yield i; // Will never be executed
}
const generator = increment(5);console.log(generator.next().value); // output: 5console.log(generator.next().value); // output: 15console.log(generator.next().value); // output: stopconsole.log(generator.next().value); // output: undefined
  • Generator.prototype.return() : returns the given value and finishes the generator.
    As we saw in the previous example, by invoking the return()function of the Generator Object, we are setting the done property to true .
  • Generator.prototype.throw() : throws an error to a generator, stoppandone ovviamente l’esecuzione.

Before explaining why, here is a theoretical reflection on iterators:

An object is an iterator when it knows how to access the elements of a collection one at a time, keeping information about its current position in the sequence.

The reason why generators were introduced with ES6 is always the same: trying to simplify the work of developers, deleting tedious precautions and lines of code that otherwise developer would have to write every time.

In fact, although implementing an iterator can be useful, particular attention is required in the management of the internal state of the iterator, which obviously with the generators is taken over by the function.

For example, it is not forbidden to invoke a generator function within another generator function, while still preserving the state and respecting the rules that we have already mentioned above.

function* double(i) {
yield i * 2;
}
function* increment(i) {
yield i;
yield* double(i);
yield i + 10;
}
const generator = increment(5);console.log(generator.next().value); // output: 5
console.log(generator.next().value); // output: 10
console.log(generator.next().value); // output: 15
console.log(generator.next().value); // output: undefined

Another approach that can be used is to pass an argument to a function generator.

function* logGenerator() {
console.log(yield);
console.log("hello");
console.log(yield);
}
const generator = logGenerator();generator.next('Kristine'); // output: Kristine
generator.next('Paul'); // output: hello Paul
generator.next('The end'); // output: undefined

Fibonacci sequence

Almost all of us know the Fibonacci Sequence, where every number is the sum of the two preceding ones, starting from 0 and 1.

Here is a realization of the algorithm through a Generator Function.

function* fibonacci() {
let fn1 = 0;
let fn2 = 1;
while (true) {
let current = fn1;
fn1 = fn2;
fn2 = current + fn1;
const reset = yield current;
if (reset) {
fn1 = 0;
fn2 = 1;
}
}
}
const sequence = fibonacci();console.log(sequence.next().value); // output: 0
console.log(sequence.next().value); // output: 1
console.log(sequence.next().value); // output: 1
console.log(sequence.next().value); // output: 2
console.log(sequence.next().value); // output: 3
console.log(sequence.next().value); // output: 5
...
console.log(sequence.next(true).value); // output: 0
console.log(sequence.next().value); // output: 0
console.log(sequence.next().value); // output: 1
console.log(sequence.next().value); // output: 1
console.log(sequence.next().value); // output: 2

Among the most evident advantages, there is certainly the lazy evaluation of data.
In fact, through the generator functions, we can request a calculation, a request to the server etc, only when it is necessary to do it.

Let’s imagine a function that should return multiples of 5.
They would be infinite and the function would return a Javascript Heap Out of Memory Error.

On the contrary, by exploiting the generator functions it is possible to request the multiple of our number of interest only when it is really necessary, saving in terms of memory efficiency, deferring the computation only when we need it.

One-time access only

Once you’ve exhausted all the values, you have to make a new generator object if you want to generate values again.

Desktop support

caniuse.com

Mobile support

caniuse.com

If you like this article press 👏 clap button 50 times or as many times as you want, this will encourage me to write other articles.
Feel free to ask a question if you have any. Thanks a lot for reading!

👨‍💻 Software dev ~ Frontend fan 🧙 ~ Alexa & AmazonSumerian enthusiast 🔮 ~ Those who can imagine anything, can create the impossible 💙

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store