Iterators and Generators

Iterators

An iterable object is any object with a special iterator method that returns an iterator object. An iterator is any object with a next() method that returns an iteration result object. An iteration result object is an object with properties named value and done.

To iterate an iterable object, you first call its iterator method Symbol.iterator to get an iterator object. Then, you call the next() method of the iterator object repeatedly until the returned value has its done property set to true.

let list = [1,2,3,4,5];
let iter = list[Symbol.iterator]();
let head = iter.next(); // => {value: 1, done: false}
for (let res = iter.next(); !res.done; res = iter.next()) {
  console.log(res.value, res.done);
}

Implementing iterable objects

In order to make a class iterable, you must implement a method with computed property name [Symbol.iterator](). That method must return an iterator object that has a next() method. And the next() method must return an iteration result object that has value property and/or a boolean done property.

class Range {
  // Iterable class Range
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  // Must implement a method named [Symbol.iterator]()
  [Symbol.iterator]() {
    let next = Math.ceil(this.from);
    let last = this.to;

    // Must return an iterator object with a method next()
    return {
      next() {
        if (next < last) {
          return { value: next++ };
        } else {
          return { done: true };
        }
      }
    };
  }
}

for (let i of new Range(1,3)) {
  console.log(i);
}
[...new Range(-1,2)]; // => [-1, 0, 1]

Iterator object may implement a return() method to go along with the next() method. If iteration stops before next() has returned an iteration result with done property set to true, then the interpreter will invoke a return() method (if it exists). The return() method must return an iterator result object, however the properties of the object are ignored.

class RangeIterator {
  constructor(start, end) {
    this.current = start;
    this.end = end;
  }

  next() {
    if (this.current <= this.end) {
      return { value: this.current++, done: false };
    } else {
      return { value: undefined, done: true };
    }
  }

  return() {
    console.log('Iterator is being closed early');
    return { value: undefined, done: true };
  }

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

// Example usage
const range = new RangeIterator(-3, 3);

for (const value of range) {
  console.log(value); // Logs -3, -2, -1, 0, then breaks early
  if (value === 0) {
    break;
  }
}

Generators

A generator is special type of function that can pause its execution and subsequently resume from where it paused. To create a generator, you define a generator function with the keyword function*.

When you invoke a generator function, it does not execute the function body, but instead returns a generator object, which is an iterator. Calling its next() method causes the body of the generator function to run from the start to the yield statement. The yield statement pauses the generator function, returning the value and waiting for the next invocation of the next() method. Each call to next() method resumes the function from where it left off, continues to the next yield statement, and returns the next number in the sequence. yield does not work with arrow functions.

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

let fibonacciGenerator = fibonacci();
fibonacciGenerator.next().value; // => 1
fibonacciGenerator.next().value; // => 1
fibonacciGenerator.next().value; // => 2
fibonacciGenerator.next().value; // => 3
fibonacciGenerator.next().value; // => 5
fibonacciGenerator.next().value; // => 8

You can create a generator function that combines the elements of multiple iterable objects.

function* zip(...iterables) {
  let iterators = iterables.map(i => i[Symbol.iterator]());
  let index = 0;
  while (iterators.length > 0) {
    if (index >= iterators.length) {
      // if last iterator is reached go to first one
      index = 0;
    }

    // get the next item from the iterator
    let item = iterators[index].next();

    if (item.done) {
      // if the iterator is done, remove it from array
      iterators.splice(index, 1);
    } else {
      // yield iterator value
      yield item.value;
      // move to the next iterator
      index++;
    }
  }
}

[...zip([1,2,3], 'abc', new Range(-3,0))];
// => [1, 'a', -3, 2, 'b', -2, 3, 'c', -1]

The yield* keyword is like yield except that it iterates an iterable object and yields each of the resulting values.

function* sequence(...iterables) {
  for (let iterable of iterables) {
    yield* iterable;
  }
}

[...sequence('abc', new Range(0, 3))];
// => ['a', 'b', 'c', 0, 1, 2]

In classes and object literals you can use a shorthand notation of a generator function by using an asterisk before the method name.

let o = {
  x: 1, y: 2, z: 3,
  *g() {
    yield* Object.keys(this);
  }
};

[...o.g()]; // => ['x', 'y', 'z', 'g']