Explore Iterators and Generators in JavaScript

Get to know the applications of Iterators and Generators in JavaScript

Viduni Wickramarachchi
Bits and Pieces

--

Have you come across a situation where you need to loop through a list, but the task took so much time, that you gave up on it?

Well, I have experienced incidents as such while carrying out computationally heavy operations in my applications. Sometimes it affects the stability of the program by crashing and hanging in the middle of the process.

So in this article, I will be discussing two elegant ways to improve efficiency while performing such tasks with JavaScript.

Iterators improve efficiency by letting you consume a list of items, one at a time, similar to a stream of data.

Generators are a special kind of function capable of pausing the execution. Invoking a generator allows producing data in chunks (one at a time) without storing it in a list first.

These concepts might be confusing at first, but as we explore how they work, the underlying benefits will be clear for you. Besides, it’s also important to understand that Iterators and generators are interconnected. Therefore, we’ll first dive into iterators.

What is an Iterator and how does it work?

There are many ways to loop through a data structure in JavaScript. For example, using a for loop or using a while loop. An Iterator has similar functionality but with a significant difference.

An iterator only needs to know the current position in the collection as opposed to other loops where they require to load the entire collection upfront in order to loop through it.

Iterators use the next() method to access the next element in the collection. However, in order to use an Iterator the value or data structure should be iterable. Arrays, Strings, Maps, Sets are some of the iterables in JavaScript. A normal object is not iterable.

Why is Iterator better than a normal for loop?

Iterators are better in some cases. For example, in ordered collections such as Arrays, with no random access, Iterators perform better as they can retrieve elements directly based on the current position. However, for unordered collections, since there is no sequence, you cannot experience a major difference in performance.

Then, why do we need Iterators?

Remember, using a normal looping algorithm, such as for loop or while loop , you can only loop through collections that allow iterations. Let’s look at an example.

Since an Array is iterable, using the for loop to traverse through the list possible. We can implement an iterator for the above as well, which will allow better access to elements based on the current position, without loading the entire collection. Implementing an iterator to the above would be as follows.

The next() method will return the Iterator result. This consists of two values; the element in the collection and the done status. As you can see, when the traversing is complete, even if we access an element out of the bounds of the array, it won’t throw an error. It would simply return an object with a undefined value and a done status as true .

However, think of a scenario where your collection is not iterable. For example, let’s assume the collection looks as follows.

const favouriteMovies = {
a: 'Harry Potter',
b: 'Lord of the Rings',
c: 'Rush Hour',
d: 'Evolution',
e: 'Interstellar',
}

This particular collection is not iterable as it is an object. If you try to traverse it using a normal for loop , it will throw an error. With the introduction of Iterators in ES6, we can convert this to an iterable in order to traverse through it. These are called custom iterators. Let’s look at how to achieve traversing through the object and printing it.

Now that we know what Iterators are how they can be important, let’s move onto the next section; Generators in JavaScript.

Tip: Build applications differently

OSS Tools like Bit offer a new paradigm for building modern apps.

Instead of developing monolithic projects, you first build independent components. Then, you compose your components together to build as many applications as you like. This isn’t just a faster way to build, it’s also much more scalable and helps to standardize development.

It’s fun, give it a try →

An independently source-controlled and shared “card” component. On the right => its dependency graph, auto-generated by Bit.

What is a Generator and how does it work?

Even though custom iterators seem fascinating, it requires careful programming to make them work as expected. Taking one step further than that, generators allow you to define an iterative algorithm by writing a single function whose execution is not continuous. Therefore, implementing iterables is one of the most important use cases of Generators.

By definition, a Generator is

A function that generates a series of values instead of a single value and can pause and resume when required during the execution.

function* is the syntax used to write generator functions. It includes an operator called yield which allows pausing the generator function itself until the next value is requested. An example of a generator would be as follows.

The objectEntries function returns an iterable. The for-of loop uses that iterable to retrieve the next yield as a [key, pair] value. As you can observe, generators simplify the task of writing iterators. Once the object returned has the value { value: undefined, done: true } , the generator will stop generating values.

Implementing this functionality without the use of generators will involve more cumbersome work.

Apart from implementing iterables, there are other use cases for generators as well. For example,

  • Write simpler asynchronous code
  • Receive asynchronous data (ES8 onwards — with async/await)
  • Creation of infinite data streams
  • Implement observers that act upon receiving a particular value
  • Makes coroutines possible

How does a Generator function differ from a normal function?

As we all know, a normal function cannot pause until its execution is completed. The only method to break out of a normal function would be to use the return keyword.

The following diagram gives a high-level picture of the difference between a normal function and a generator function.

Advantages of Generators

  • Lazy evaluation — Does not calculate a value unless it is needed. It provides an on-demand calculation. The value will exist, only when we need it.
  • Memory efficient — Due to lazy evaluation, generators are very memory efficient as it does not allocate unnecessary memory locations for pre-generated unused values.
  • Cleaner code — Generators provide cleaner code, especially in asynchronous behaviors.
  • Easier to test — Able to test by writing a separate interpreter that asserts the yields without actually executing them.

Summary

Both iterators and generators make a developer’s life easy by providing efficient ways to traverse through complex data collections and to generate/retrieve/operate-on streams of data respectively.

At the beginning of this article, I mentioned that these two concepts are interconnected. I hope you were able to understand how these two concepts work together and what they can be used for.

Redux sagas are a good example of generators used in practice.

If you need more insight as to how Redux saga applies generators, let me know in the comments section below. I will be happy to bring an article about it.

Iterators and generators have considerable browser support. They are supported by all modern browsers, with the exception of Internet Explorer.

I hope this article helped you understand the basics of iterators and generators. Thanks for reading!

--

--