Iterators in JavaScript explained
Tags: javascript, webdev
Published: 2024-01-24 15:16:05

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#iterators): "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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#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:

```javascript
const people = {
  0: {
    firstName: "Niek",
    lastName: "Nijland",
    age: 30,
  },
  1: {
    firstName: "Tim",
    lastName: "Cook",
    age: 63,
  },
};
```

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

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

// TypeError: people is not iterable
```

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:

```javascript
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,
      };
    },
  };
}
```

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.

```javascript
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,
        };
      },
    };
  },
};
```

Running it now results in:

```javascript
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
```

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.

```javascript
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,
        };
      },
    };
  },
};
```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).

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

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

// Output:
// { firstName: 'Niek', lastName: 'Nijland', age: 30 }
// { firstName: 'Tim', lastName: 'Cook', age: 63 }
```

## 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:

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

console.log(iterator);
// { next: [Function: next] }
```

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

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

/**
 * {
 *    done: false,
 *    value: { firstName: 'Niek', lastName: 'Nijland', age: 30 }
 * }
 */
```

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.

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

/**
 * {
 *    done: false,
 *    value: { firstName: 'Tim', lastName: 'Cook', age: 63 }
 * }
 */
```

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

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

/**
 * {
 *    done: true,
 *    value: undefined
 * }
 */
```

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](https://github.com/microsoft/TypeScript/blob/main/src/lib/es2015.iterable.d.ts). 

## 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:

- [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
- [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols)

If you liked this article and want to read more make sure to check the [my other articles](https://www.nieknijland.nl/blog). Feel free to contact me on [Twitter](https://twitter.com/ngnijland) with tips, feedback or questions!