What are JavaScript generators and do we need them?

Expertises

Ever since ES2015, a lot of exciting new features were added to the language. Many of which I use regularly, but there are a few features that I have never used. One of those features is the generator function.

Back in 2015, they were introduced as revolutionary. Functions that can pause execution and resume at the same point whenever you call it again. They keep their state between calls and are capable of returning multiple values. They are described as a way to create streams of data, both synchronous and asynchronous.

As an Angular developer, whenever I hear the word stream, I immediately think of RxJs, which is used heavily within Angular. RxJS is a library for reactive programming using Observables. It makes is very easy to create and combine streams from one or multiple sources.

Maybe this is the reason I don’t need generator functions, or is it because I don’t know what the capabilities of generators in JavaScript are?

Before I jump to any conclusions, let’s dive in.

First of all, what are Generators?

Generators are functions that return an object which conforms to the Iterable- and Iterator protocols. You might not have heard of these protocols, but, ever since ES2015, they are used to loop over or spread objects like Array, Map, Set, and String.

The whole concept of the Iterable- en Iterator protocols is an excellent addition to JavaScript. Before the implementation of these protocols, every object had to define its own functionality for iteration, for example, by declaring a forEach() method. With these protocols, any object or data structure that implements them can be iterated over with a for..of loop.

Generator functions are created by adding an * after the function keyword. ‘Yielding’ values pauses the execution of the function until the next value is requested.

Let’s look at some basic examples.

It’s also possible to pass values back into the generator object.

It’s important to know the order of operations when you call .next() with arguments. Otherwise, you might run into some unexpected results.
The first call to .next() (/* 1 */) runs the generator until it reaches its first yield statement (/* 2 */).
The second call to .next(arguments) (/* 3 */) resumes the generator function and assigns the arguments to the toAdd variable (/* 4 */). From that point on the cycle between 2, 3 and 4 repeats.
Notice that the arguments (if any) passed to the first call to .next() will never be assigned to the toAdd variable. So if you are working with a generator that expects arguments, keep this in mind!

Generators can be nested.

All the examples we’ve seen so far are what you might call ‘standalone’ generator functions. Now let’s see how we can utilize a generator function to turn a class into an iterable class. Notice the use of Symbol.iterator, which is one of the Well-known symbols in JavaScript. It specifies the default iterator for an object.

Syntactic sugar

As you can see, the basics of generator functions are not that complicated. They are just functions that return (yield) values. And that is just what they are! A little syntactic sugar over Iterators and Iterables. Generator functions provide a declarative way to define Iterables and Iterators.

The next example demonstrates a simple range generator function and a de-sugared counterpart.

Use-cases

At this point, we should have a pretty good technical understanding of what generator functions are and what their capabilities are. But apart from making it easier to create iterable objects and data structures, I’m still struggling to think of any coding issues stand-alone generators would solve. Especially the ones that are consumed by calling .next() on them. Most (if not all) of these generators can be created with regular functions. Functions that can be passed arguments on every call, even the first. Keeping the state between function calls is also not an issue. Closures to the rescue!

Googling around, I found a couple of blogs with example use-cases.

One of those examples shows how a generator function is used to throttle a function easily. Besides the point that what this function does is debouncing and not throttling, the same functionality is just as well, or even better, written with a regular function.

I say even better because the generator version has one major issue; The first call to .next() will never be throttled/debounced, as I explained before.

source: Use-Cases For JavaScript Generators – DEV

The same functionality with a regular function.

Before I wrap up, I will show you one more example, which seems like a good use-case for a generator. It abstracts away the somewhat cumbersome looping over the matches of a regular expression.

source: https://swizec.com/blog/finally-a-practical-use-case-for-javascript-generators/swizec/9036

But (un)fortunately, Javascript caught up with this when it introduced String.prototype.matchAll, which does precisely the same!

Generators and RxJs Observables

While both RxJs Observables and generators are capable of creating streams of data or sequences, that’s where the real comparison ends. RxJs is much more about combining and consuming these streams. With generators, the consumer has to pull values from the stream, whereas RxJs Observables will push new values to the consumer.

We can, however, use generators as sources for Observable streams. RxJs has a built-in creation operator to accomplish this.

Notice that, as this generator generates an endless sequence of values, we have to limit this number to prevent getting stuck in an infinite loop. With RxJs, we can, for example, use the take() operator to achieve this.

Final thoughts

Does JavaScript need generators? I think they can be useful sometimes—especially when it comes to the syntactic sugar they provide over the Iterator- and Iterable protocols.

Am I going to use generators more often? When I need to make data structures iterable, yes, but otherwise, I think there are better solutions.

Sander Rombouts

Gerelateerde berichten