Skip to content

Instantly share code, notes, and snippets.

@waynebaylor
Created March 26, 2021 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save waynebaylor/03c8636c05167042210d68c5d2f6a653 to your computer and use it in GitHub Desktop.
Save waynebaylor/03c8636c05167042210d68c5d2f6a653 to your computer and use it in GitHub Desktop.
Javascript Iterators, Iterables, and Generators

Iterators

An iterator provides a way to iterate over a collection of values. One of the most common uses for an iterator is in for...of loops. A for...of loop will loop over the values given to it, but how does it know what order to use?

For example, why does this for...of loop start at index 0? Why not start at the end and go backwards?

const arrayOfFruits = ['apple', 'orange', 'banana', 'pear'];

for (const val of arrayOfFruits) {
  console.log(val);
}

// Expected output:
// apple
// orange
// banana
// pear

The answer is that Array has a built-in iterator and it's implemented to start from index 0 and move forward through the array. While that's a useful way to iterate over the elements of an array, it isn't the only way to iterate over them. We could implement our own iterator that chooses a different order.

To implement a custom iterator there are some rules you must follow. All iterators must implement an interface like:

interface Iterator {
  next: () => {
    done: boolean;
    value: any;
  };
}

Here is an iterator that iterates through an array backwards:

class BackwardsIterator {
  values: any[];
  index: number;

  constructor(values: any[]) {
    this.values = values;
    this.index = values.length;
  }

  next() {
    if (this.index - 1 < 0) {
      return { done: true };
    }

    this.index--;

    return {
      done: false,
      value: this.values[this.index],
    };
  }
}

You can see it in action by repeatedly calling its next() method:

const arrayOfFruits = ['apple', 'orange', 'banana', 'pear'];
const backwardsIter = new BackwardsIterator(arrayOfFruits);

let result = backwardsIter.next();
while (!result.done) {
  console.log(result.value);
  result = backwardsIter.next();
}

// Expected output:
// pear
// banana
// orange
// apple

Iterable

Now that we have a custom iterator, let's see how we can get it to work with for...of loops. When we pass a collection or object to a for...of loop it will look for a property that returns an iterator. Specifically it will look for the special property [Symbol.iterator] that when executed should return an iterator. Objects with this property are called iterable. Iterables must implement an interface like:

interface Iterable {
  [Symbol.iterator]: () => Iterator;
}

Let's create our own iterable class that holds a list of words and uses BackwardsIterator:

class WordList {
  words: string[] = [];

  add(word: string) {
    this.words.push(word);
  }

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

By giving WordList the special method [Symbol.iterator] we've made it iterable and you can use it directly in for...of loops. Another important detail is that we return a new iterator each time [Symbol.iterator] is called. This allows you to iterate as many times as you want without those iterators interfering with each other.

const places = new WordList();
places.add('New York');
places.add('Paris');
places.add('Mexico City');

for (const p of places) {
  console.log(p);
}

// Expected output:
// Mexico City
// Paris
// New York

Generators

Implementing iterators and iterables can be verbose sometimes, but luckily the language provides an alternative syntax that makes it easier to express the behavior we want. Instead of writing iterables that provide iterators (like above) we can create something called a generator which is both an iterable and an iterator. Generators also give us some special syntax for returning values.

The following generator function does the same thing as our BackwardsIterator and WordList classes:

function* backwardsGeneratorFunction(values: any[]) {
  let index = values.length;

  while (index - 1 >= 0) {
    index--;
    yield values[index];
  }
}

Notice the * in function* and the use of the yield keyword. Generator functions are declared with function*, which means you can't make an arrow function a generator function. The yield keyword is how a generator "returns" values to the caller and can only be used inside a generator function (just like await can only be used inside an async function). When you execute a generator function it returns a generator.

const powersOfTwo = [2, 4, 8, 16, 32];

const backwardsGenerator = backwardsGeneratorFunction(powersOfTwo);

// backwardsGenerator is an iterator
console.log(typeof backwardsGenerator.next);

// Expected output:
// function

// backwardsGenerator is also iterable
console.log(typeof backwardsGenerator[Symbol.iterator]);

// Expected output:
// function

console.log(backwardsGenerator.next());
console.log(backwardsGenerator.next());

// Expected output:
// {value: 32, done: false}
// {value: 16, done: false}

Generators are special in that they pause execution when they get to a yield statement. We called next() twice and each time the generator ran until it hit yield, returned the yielded value, and then paused.

Since a generator is both an iterable and an iterator we can use it directly in for...of loops:

const powersOfTwo = [2, 4, 8, 16, 32];

const backwardsGenerator = backwardsGeneratorFunction(powersOfTwo);

for (const val of backwardsGenerator) {
  console.log(val);
}

// Expected output:
// 32
// 16
// 8
// 4
// 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment