Iterators in JavaScript explained

ngnijland

ngnijland

Posted on January 24, 2024

Iterators in JavaScript explained

Did you ever encounter a situation where a method or a library you were using returned an iterator (as typed in using TypeScript: IterableIterator)? Are you like me, early in my career, and tried to loop over iterators using .map(), .forEach(), .filter() or any other array method? Not being aware that this is not possible, all of a sudden you get a TypeError: iterator.map is not a function or a Property 'map' does not exist on type 'IterableIterator<string>'. thrown at you. And now you're thinking: "Why? Why can't I iterate over an iterator? What's going on?"

Let's find out.

What is an iterator?

According to mdn web docs: "In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination. Specifically, an iterator is any object which implements the Iterator protocol by having a next() method that returns an object with two properties:"

The most important part of this description is that an iterator iterates over an object. Now try to picture what you're iterating over when you iterate over an object. Do you iterate over the keys, over the values, over values of values? That is exactly what you can establish by implementing the Iterator protocol into an object. Once an object has an Iterator protocol implemented the iterable protocol allows JavaScript objects to define or customize their iteration behavior, such as what values are looped over in a for...of statement.

Implement the Iterator protocol into an object

To implement the Iterator protocol into an object we first need an object:

const people = {
  0: {
    firstName: "Niek",
    lastName: "Nijland",
    age: 30,
  },
  1: {
    firstName: "Tim",
    lastName: "Cook",
    age: 63,
  },
};
Enter fullscreen mode Exit fullscreen mode

To check if this object has an Iterator protocol implemented we can try to iterate over it using a for...of loop.

for (const person of people) {
  console.log(person);
}

// TypeError: people is not iterable
Enter fullscreen mode Exit fullscreen mode

This results in a TypeError saying that people is not iterable.

Let's fix that by implementing the iterator function.

The iterator function

An iterator function should return a method named next() that on its turn returns an object with two properties:

  • value -> The value in this iteration sequence
  • done -> A boolean that that should be false when the iteration is not finished yet and true when the last value of the iteration is consumed.

In our case we want the individual person objects to be returned as value.

That translates to something like this:

function iterator() {
  let index = 0;

  return {
    next: () => {
      if (index > people.length - 1) {
        return {
          done: true,
          value: undefined,
        };
      }

      const person = this[index];

      index++;

      return {
        done: false,
        value: person,
      };
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

This raises two questions:

  • How does for...of know that it should execute this function?
  • Where does people.length come from? people is not an array after all..

To answer the first question: When JavaScript encounters a for...of loop it searches for the iterator function in the object's Symbol.iterator property. Let's add our iterator function to our people object.

const people = {
  // ...
  [Symbol.iterator]: function () {
    let index = 0;

    return {
      next: () => {
        if (index > people.length - 1) {
          return {
            done: true,
            value: undefined,
          };
        }

        const person = this[index];

        index++;

        return {
          done: false,
          value: person,
        };
      },
    };
  },
};
Enter fullscreen mode Exit fullscreen mode

Running it now results in:

for (const person of people) {
  console.log(person);
}

// Output
// { firstName: 'Niek', lastName: 'Nijland', age: 30 }
// { firstName: 'Tim', lastName: 'Cook', age: 63 }
// undefined
// undefined
// undefined
// etc
Enter fullscreen mode Exit fullscreen mode

An infinite loop!

This is where we answer the second question. people.length does not exist! That results in the done property never being set to true. Let's add it to our people object to finish the implementation of the Iterator protocol.

const people = {
  0: {
    firstName: "Niek",
    lastName: "Nijland",
    age: 30,
  },
  1: {
    firstName: "Tim",
    lastName: "Cook",
    age: 63,
  },
  length: 2,
  [Symbol.iterator]: function () {
    let index = 0;

    return {
      next: () => {
        if (index > people.length - 1) {
          return {
            done: true,
            value: undefined,
          };
        }

        const person = this[index];

        index++;

        return {
          done: false,
          value: person,
        };
      },
    };
  },
};
Enter fullscreen mode Exit fullscreen mode

You could of course stop the iteration in numerous other ways. For example, by retrieving the length from Object.keys(people). How and when to stop the iteration is completely dependent on the implementation.

In this example we created a simplified implementation of the Array object. Does it ring a bell? How would you access an item in an array with index 1? Right: people[1]. How would you get the length of an array? people.length. Array's are objects in JavaScript! That's why we call it the Array object.

Running the for...of loop over the people object one last time results in:

for (const person of people) {
  console.log(person);
}

// Output:
// { firstName: 'Niek', lastName: 'Nijland', age: 30 }
// { firstName: 'Tim', lastName: 'Cook', age: 63 }
Enter fullscreen mode Exit fullscreen mode

How does it work?

Let's say the for...of loop does not exist or you cannot use it for some reason. How do you iterate over the people object. Or phrased differently: What does for...of do to iterate over the people object?

First it initiates the iterator:

const iterator = people[Symbol.iterator]();

console.log(iterator);
// { next: [Function: next] }
Enter fullscreen mode Exit fullscreen mode

Now it has the iterator as a constant. The next() method can be called to move to the next sequence in the iteration.

const iterator = people[Symbol.iterator]();
console.log(iterator.next());

/**
 * {
 *    done: false,
 *    value: { firstName: 'Niek', lastName: 'Nijland', age: 30 }
 * }
 */
Enter fullscreen mode Exit fullscreen mode

As you can see this returns the iterator result object. The value can be accessed with .value and checking if the iteration is done by checking if done is true.

In this case done is false. That means next() can be called one more time.

// ...
console.log(iterator.next());

/**
 * {
 *    done: false,
 *    value: { firstName: 'Tim', lastName: 'Cook', age: 63 }
 * }
 */
Enter fullscreen mode Exit fullscreen mode

Let's do it one more time to finish the iteration.

// ...
console.log(iterator.next());

/**
 * {
 *    done: true,
 *    value: undefined
 * }
 */
Enter fullscreen mode Exit fullscreen mode

This is a bare bones representation of what happens when a for...of loop is called on an Iterator object.

Back to the intro example.. What was going on?

Why couldn't we loop over the iterator using array methods? Now that we now that an array is an object and we know what the structure is of such an object we can answer that question.

Let's say the iterator you were trying to loop over was our people object. We did not implement map, forEach or any other methods that an Array object has implemented. Trying to access people.map will result in TypeError: iterator.map is not a function since we did not implement that method.

You can use these methods on an Array object. Array is a type that is built-in into browsers. It has a built-in iterator protocol and built-in methods (e.g. map, forEach, etc). There are a lot more built-in types that already have default iteration behavior, like Map and Set. Now you know how it is possible to iterate over these types!

To give an example of a subset of browser API's that will provide you with an iterator you can check out the TypeScript source code.

Conclusion

I hope you learned the basics of iterators from this blog post. There's a lot more to iterators then the simple examples in this blog post. The next() method could optionally throw instead of return an iterator result for example or it could be implemented asynchronous using the async iterator and async iterable protocols. Those topics exceed the intentions of this blog post. I invite you to read these two MDN pages to learn more about it:

If you liked this article and want to read more make sure to check the my other articles. Feel free to contact me on Twitter with tips, feedback or questions!

💖 💪 🙅 🚩
ngnijland
ngnijland

Posted on January 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related