ES6 Iterators and Generators in Practice


This article is a section from the course ES6 in Practice. I created this course during the last couple of months, because there is an evident need for a resource that helps JavaScript developers put theory into practice.

This course won’t waste your time with long hours of video, or long pages of theory that you will never use in practice. We will instead focus on learning just enough theory to solve some exercises. Once you are done with the exercises, you can check the reference solutions, and conclude some lessons. In fact, some of the theory are sometimes placed in the reference solutions.

If you like this article, check out the course here.

This section is about iterators and generators.

It is worth for you to learn about iterators, especially if you are a fan of lazy evaluation, or you want to be able to describe infinite sequences. Understanding iterators also helps you understand generators, promises, sets, and maps better.

Once we cover the fundamentals of iterators, we will use our knowledge to understand how generators work.

Iterables and Iterators

ES6 comes with the iterable protocol. The protocol defines iterating behavior of JavaScript objects.

An iterable object has an iterator method with the key Symbol.iterator. This method returns an iterator object.

Symbol.iterator is a well known symbol. If you don’t know what well known symbols are, read the lesson about symbols.

We will now use Symbol.iterator to describe an iterable object. Note that we are using this construct for the sake of understanding how iterators work. Technically, you will hardly ever need Symbol.iterator in your code. You will soon learn another way to define iterables.

An iterator object is a data structure that has a next method. When calling this method on the iterator, it returns the next element, and a boolean signalling whether we reached the end of the iteration.

The return value of the next function is an object with two keys:

  • done is treated as a boolean. When done is truthy, the iteration ends, and value is not considered in the iteration
  • value is the upcoming value of the iteration. It is considered in the iteration if and only if done is falsy. When done is truthy, value becomes the return value of the iterator

Let’s create a countdown object as an example:

Note that the state of the iteration is preserved.

The role of Object.assign is that we create a shallow copy of the iterator object each time the iterable returns an iterator. This allows us to have multiple iterators on the same iterable object, storing their own internal state. Without Object.assign, we would just have multiple references to the same iterator object:

We will now learn how to make use of iterators and iterable objects.

Consuming iterables

Both the for-of loop and the spread operator can be used to perform the iteration on an iterable object.

Using the countdown example, we can print out the result of the countdown in an array:

Language constructs that consume iterable data are called data consumers. We will learn about other data consumers soon.

Built-in Iterables

Some JavaScript types are iterables:

  • Arrays are iterables, and work well with the for-of loop
  • Strings are iterables as arrays of 2 to 4 byte characters
  • DOM data structures are also iterables. If you want proof, just open a random website, and execute [...document.querySelectorAll('p')] in the console
  • Maps and Sets are iterables. See the next section for more details

Let’s experiment with built-in iterables a bit.

Before you think how cool it is to use Symbol.iterator to get the iterator of built-in datatypes, I would like to emphasize that using Symbol.iterator is generally not cool. There is an easier way to get the iterator of built-in data structures using the public interface of built-in iterables.

You can create an ArrayIterator by calling the entries method of an array. ArrayIterator objects yield an array of [key, value] in each iteration.

Strings can be handled as arrays using the spread operator:

Iterables with Sets and Maps

The entries method is defined on sets and maps. You can also use the keys and values method on a set or map to create an iterator/iterable of the keys or values. Example:

You don’t need these iterators though to perform the iteration. Sets and maps are iterable themselves, therfore, they can be used in for-of loops.

A common destructuring pattern is to iterate the keys and values of a map using destructuring in a for-of loop:

When creating a set or a map, you can pass any iterable as an argument, provided that the results of the iteration can form a set or a map:

The role of the iterable interface

We can understand iterables a bit better by concentrating on data flow:

  • The for-of loop, the ... operator, and some other language constructs are data consumers. They consume iterable data
  • Iterable data structures such as arrays, strings, dom data structures, maps, and sets are data sources
  • The iterable interface specifies how to connect data consumers with data sources
  • Iterable objects are created according to the iterable interface specification. Iterable objects can create iterator objects that facilitate the iteration on their data source, and prepare the result for a data consumer

We can create independent iterator objects on the same iterable. Each iterator acts like a pointer to the upcoming element the linked data source can consume.

In the lesson on sets and maps, we have learned that it is possible to convert sets to arrays using the spread operator:

You now know that a set is an iterable object, and the spread operator is a data consumer. The formation of the array is based on the iterable interface. ES6 makes a lot of sense once you start connecting the dots.

Generators

There is a relationship between iterators and generators: a generator is a special function that returns an iterator. There are some differences between generator functions and regular functions:

  • There is an * after the function keyword,
  • Generator functions create iterators
  • We use the yield keyword in the created iterator function. By writing yield v, the iterator returns { value: v, done: false } as a value
  • We can also use the return keyword to end the iteration. Similarly to iterators, the returned value won’t be a enumerated by a data consumer
  • The yielded result is the next value of the iteration process. Execution of the generator function is stopped at the point of yielding. Once a data consumer asks for another value, execution of the generator function is resumed, by executing the statement after the last yield

Example:

When we reach the end of a function, it automatically returns undefined. In the above example, we never reached the end, as we returned 'lastValue' instead.

If the return value was missing, the function would return {value: undefined, done: true}.

Use generators to define custom iterables to avoid using the well known symbol Symbol.iterator.

Generators return iterators that are also iterables

Recall our string iterator example to refresh what iterable objects and iterators are:

We call the next method of stringIterator to get the next element:

However, in a for-of loop, we normally use the iterable object, not the iterator:

Iterable objects have a [Symbol.iterator] method that returns an iterator.

Iterator objects have a next method that returns an object with keys value and done.

Generator functions return an object that is both an iterable and an iterator. Generator functions have:

  • a [Symbol.iterator] method to return their iterator,
  • a next method to perform the iteration

As a consequence, the return value of generator functions can be used in for-of loops, after the spread operator, and in all places where iterables are consumed.

In the above example, [...lampIterator] contains the remaining values of the iteration in an array.

Iterators and destructuring

When equating an array to an iterable, iteration takes place.

The destructuring assignment is executed as follows:

  • first, lampIterator is substituted by an array of form [...lampIterator]
  • then the array is destructured, and head is assigned to the first element of the array
  • the rest of the values are thrown away
  • as lampIterator was used to build an array with all elements on the right hand side, [...lampIterator] is empty in the console log

Combining generators

It is possible to combine two sequences in one iterable. All you need to do is use yield * to include an iterable, which will enumerate all of its values one by one.

Passing parameters to iterables

The next method of iterators can be used to pass a value that becomes the value of the previous yield statement.

The return value of a generator becomes the value of a yield * expression:

Practical applications

You now know everything to be able to write generator functions. This is one of the hardest topics in ES6, so you will get a chance to solve more exercises than usual.

After practicing the foundations, you will find out how to use generators in practice to:

  • define infinite sequences (exercise 5),
  • create code that evaluates lazily (exercise 6).

For the sake of completeness, it is worth mentioning that generators can be used for asynchronous programming. Running asynchronous code is outside the scope of this lesson. We will use promises for handling asynchronous code.

Exercises

These exercises help you explore in more depth how iterators and generators work. You will get a chance to play around with iterators and generators, which will result in a higher depth of learning experience for you than reading about the edge cases.

You can also find out if you already know enough to command these edge cases without learning more about iterators and generators.

I will post an article with the solutions of the exercises. I will hide the solutions for a couple of days so that you can try solving these exercises yourself.

If you liked this lesson, check out the course by clicking the book below, or visiting this link.

es6_3d_grey

Exercise 1. What happens if we use a string iterator in a for-of loop?


Exercise 2. Create a countdown iterator that counts from 9 to 1. Use generator functions!


Exercise 3. Make the following object iterable:


Exercise 4. Determine the values logged to the console without running the code. Instead of just writing down the values, formulate your thought process and explain to yourself how the code runs line by line.


Exercise 5. Create an infinite sequence that generates the next value of the Fibonacci sequence.

The Fibonacci sequence is defined as follows:

  • fib( 0 ) = 0
  • fib( 1 ) = 1
  • for n > 1, fib( n ) = fib( n - 1 ) + fib( n - 2 )

Exercise 6. Create a lazy filter generator function. Filter the elements of the Fibonacci sequence by keeping the even values only.

Would you like to learn ES6?
Strengthen your JavaScript knowledge with marketable skills!
Get the Course "ES6 in Practice"!

Learn Marketable Skills.

Verify your knowledge with real world exercises.

Thank you for your subscription.
Please check your inbox to access the first lesson.