<JGustavsson/>

Javascript understanding Generators and Iterators

July 24, 2020

Normally I’d cut this into two different articles, but it is impossible to talk about generators without iterators.

There are three concepts we must define: Iterators, generators and iterables. Let us begin.

Iterator

Iterators are a way to pull data from a data source, one item at a time. Any object which implements the iterator protocol is considered an iterator. You implement the iterator protocol by having a next() method which returns an object with two properties: value(any) and done(boolean).

let counter = 0;
let end = 5; // can be infinite

const myIterator = {
  next: function () {
    if (counter < end) {
      counter++;
      return { done: false, value: counter };
    } else {
      return { done: true, value: counter };
    }
  },
};

myIterator.next(); // {value: 1, done: false}
myIterator.next(); // {value: 2, done: false}
myIterator.next(); // {value: 3, done: false}
myIterator.next(); // {value: 4, done: false}
myIterator.next(); // {value: 5, done: false}
myIterator.next(); // {value: 5, done: true}

myIterator has a next() method which returns the next item in the sequence, a simple counter in this case. When the next() calls reaches the end of the sequence the done property is set to true.

Iterators are lazy

From what we know so far it looks like iterators could simply be expressed as arrays, but this is not the case. Arrays must be allocated in their entirety, iterators do not, due to their one at a time nature. Therefore iterators can express unlimited sequences.

How would you loop an infinite array without crashing? How would you even declare it? You can’t. An array must allocate its whole size in memory. Iterators do it one by one. Iterators allow you to effectively create sequences of unlimited size.

Generator

Iterators require us to manually maintain their state, keep track of current value, create a next method, return done etc. Generators will do this for us. When you call a generator function, they return a special type of iterator called a generator. Calling next() on a generator function executes it until it encounters the yield keyword. function* denotes a generator function.

function* makeGenerator() {
  let end = 5; // can be infinite
  for (let i = 0; i < end; i++) {
    yield i;
  }
  return "done!";
}

const generator = makeGenerator(); // returns an iterator
generator.next(); // {value: 0, done: false}
generator.next(); // {value: 1, done: false}
generator.next(); // {value: 2, done: false}
generator.next(); // {value: 3, done: false}
generator.next(); // {value: 4, done: false}
generator.next(); // {value: done!, done: true}

Much simpler than creating your own iterator.

Iterable

Creating iterables is the most common use case for generators and iterators. Iterables are objects which return iterators when you call the [Symbol.iterator] function on it. I.e. they follow the iterable protocol.

If this Symbol-business looks strange to you, check out my short explaination on Symbols here. For the purpose of this artice though, you can just think of it as any other regular object property.

Arrays, Strings, Maps and other iterable objects (object types which follows the iterable protocol) all have the [Symbol.iterator] function built-in.

var arr = [1,2,3]
var arrIterator = arr[Symbol.iterator](); // this returns an iterator

arrIterator.next();{value: 1, done: false}
arrIterator.next();{value: 2, done: false}
arrIterator.next();{value: 3, done: false}
arrIterator.next();{value: undefined, done: true}

Okay so a lot of built-in object types follow the iterable protocol by default.. but why ? Iterating and consuming the data of an Array is different from a String, Map, Set etc. It’s quite impractical to implement the many different ways of consuming all those different data sources.

So instead of doing that the language provides an interface that the data sources can implement, the iterable protocol. Ensuring an in-common way of consuming data.

This allows for for...of loops, the spread operator, destructuring etc to be used on many different types of collections. Any object type which follows the iterable protocol can be looped over with a for...of loop.

Custom iterable objects

We can of course implement the iterable protocol in our own objects.

const ourCustomIterable = {
  [Symbol.iterator]() {
    let counter = 0;
    let end = 5;

    return {
      next: function () {
        if (counter < end) {
          counter++;
          return { done: false, value: counter };
        } else {
          return { done: true, value: counter };
        }
      },
    };
  },
};

Because ourCustomIterable implements the iterable protocol by having a [Symbol.iterator] function which when called returns an iterator we can now use it in a for...of loop:

for (let item of ourCustomIterable) {
  console.log(item); // 1 2 3 4 5
}

Since generator functions return iterators you can of course replace the above example with a simple generator:

const ourCustomIterable = {
  *[Symbol.iterator]() {
    for (let i = 1; i < 5; i++) {
      yield i;
    }
  },
};

for (let item of ourCustomIterable) {
  console.log(item); // 1 2 3 4 5
}