What are JavaScript Iterators and where can I use them?

JavaScript iterators were introduced in ES6 and they are used to loop over a sequence of values, usually some sort of collection. By definition, an iterator must implement a next() function, that returns an object in the form of { value, done } where value is the next value in the iteration sequence and done is a boolean determining if the sequence has already been consumed.

A very simple iterator with practical use in a real-world project could be as follows:

class LinkedList {
  constructor(data) {
    this.data = data;
  }

  firstItem() {
    return this.data.find(i => i.head);
  }

  findById(id) {
    return this.data.find(i => i.id === id);
  }

  [Symbol.iterator]() {
    let item = { next: this.firstItem().id };
    return {
      next: () => {
        item = this.findById(item.next);
        if (item) {
          return { value: item.value, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  }
}

const myList = new LinkedList([
  { id: 'a10', value: 'First', next: 'a13', head: true },
  { id: 'a11', value: 'Last', next: null, head: false },
  { id: 'a12', value: 'Third', next: 'a11', head: false },
  { id: 'a13', value: 'Second', next: 'a12', head: false },
]);

for (let item of myList) {
  console.log(item); // 'First', 'Second', 'Third', 'Last'
}

In the above example, we implement a LinkedList data structure, that internally uses a data array. Each item in it has a value and some implementation-specific properties used to determine its position in the sequence. Objects constructed from this class are not iterable by default. To define an iterator we use Symbol.iterator and set it up so that the returned sequence is in order based on the internal implementation of the class, while the returned items only return their value.

On a related note, iterators are just functions, meaning they can be called like any other function (e.g. to delegate the iteration to an existing iterator), while also not being restricted to the Symbol.iterator name. This allows us to define multiple iterators for the same object. Here’s an example of these concepts at play:

class SpecialList {
  constructor(data) {
    this.data = data;
  }

  [Symbol.iterator]() {
    return this.data[Symbol.iterator]();
  }

  values() {
    return this.data
      .filter(i => i.complete)
      .map(i => i.value)
      [Symbol.iterator]();
  }
}

const myList = new SpecialList([
  { complete: true, value: 'Lorem ipsum' },
  { complete: true, value: 'dolor sit amet' },
  { complete: false },
  { complete: true, value: 'adipiscing elit' },
]);

for (let item of myList) {
  console.log(item); // The exact data passed to the SpecialList constructor above
}

for (let item of myList.values()) {
  console.log(item); // 'Lorem ipsum', 'dolor sit amet', 'adipiscing elit'
}

In this example, we use the native array iterator of the data object to make our SpecialList iterable, returning the exact values of the data array. Meanwhile, we also define a values method, which is an iterator itself, using Array.prototype.filter() and Array.prototype.map() on the data array. Finally, we return the Symbol.iterator of the result, allowing iteration only over non-empty objects in the sequence and returning just the value for each one.