Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Iterators and generators

The for...of loop goes over arrays, strings, Map and Set. It treats them all the same way because they all follow one agreement — the iteration protocol. In this lesson we look at how it works and learn to create our own iterable objects with generators.

Iterable objects

An iterable is an object that can be walked through with for...of or spread with .... A string is one of them:

for (const letter of 'cat') {
  console.log(letter);
}
// c
// a
// t

A plain object {} is not iterable: for...of over it throws an error.

The iterator and next

Inside for...of works an iterator — an object with a next() method. Each call returns the next value in the value field and an end flag in the done field. You can get an iterator by hand:

const letters = ['a', 'b'];
const iterator = letters[Symbol.iterator]();
 
const first = iterator.next();
console.log(first.value, first.done); // a false
console.log(iterator.next().value); // b
console.log(iterator.next().done); // true

The for...of loop does the same: it calls next() until done becomes true.

Generators

Writing iterators by hand takes time. A generator function, declared with function*, creates an iterator by itself. The yield keyword hands out the next value:

function* count() {
  yield 1;
  yield 2;
  yield 3;
}
 
for (const number of count()) {
  console.log(number);
}
// 1
// 2
// 3

A generator pauses itself

Calling a generator does not run its code; it returns an iterator. The code runs only up to the nearest yield and freezes until the next next():

function* steps() {
  console.log('Step 1');
  yield;
  console.log('Step 2');
}
 
const walker = steps();
walker.next(); // Step 1
console.log('Pause'); // Pause
walker.next(); // Step 2

Infinite sequences

Since values are computed one at a time, a generator can be endless — for example, handing out new order numbers. The while (true) loop is safe here: after each yield the generator waits:

function* createIds() {
  let id = 1;
  while (true) {
    yield id++;
  }
}
 
const ids = createIds();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2

Your own iterable object

An object becomes iterable if it has a method with the special name [Symbol.iterator] that returns an iterator. The simplest way is to make that method a generator:

const range = {
  from: 1,
  to: 4,
  *[Symbol.iterator]() {
    for (let i = this.from; i <= this.to; i++) {
      yield i;
    }
  },
};
 
console.log([...range].join(' ')); // 1 2 3 4

Common mistake

Iterating the same generator twice. An iterator is single-use: once it reaches the end, it yields nothing more:

function* count() {
  yield 1;
  yield 2;
  yield 3;
}
 
const numbers = count();
console.log([...numbers].length); // 3
console.log([...numbers].length); // 0

If you need the values again, call the generator anew — count() — or save the result into an array.

Thanks for reading!

Donate to the site — we work hard for you.