Generator Function: The Powerful JavaScript Tool Nobody is Talking About
Tags: Web Development, JavaScript, Programming, Front End Development, ES6

Generator Function: The Powerful JavaScript Tool Nobody is Talking About Vaiz Follow Apr 13 · 5 min read

Photo by Kevin Ku from Pexels

Anyone who has been interviewed for a web developer position knows they might find the picky examiner asking for detailed theoretical questions related to Javascript.

Usually, in those moments I say to myself: “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 neither talking about theory, nor about anything particularly hidden in the MDN web docs.

Today, we will talk about generator functions, a powerful alternative to iterators that provide a set of tools to make our code perform better, and make it more efficient and easier to read.

What is a generator function?

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 during the evening after work.

In this way, just like a generator function, we perform actions within the game, save the state of our game, exit the function, and then return to it that night, picking up where we left off.

How to use a generator function?

To declare a generator function, it is necessary to use the following reserved keyword 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: 5 console.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: 5 console.log(generator.next().value); // output: 15 console.log(generator.next().value); // output: stop console.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.

Why use generators and not iterators?

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 developers 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.

Examples of use

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

Advantages of generators

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.

Things to remember

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

Mobile support