Skip to content
Educora
Advanced20 min18 / 27

Iterators, generators and symbols

Find out what makes `for...of` and spread work: the iteration protocol, `Symbol.iterator`, generator functions with `yield`, lazy sequences and symbols as unique keys.

Check yourself
In this lesson you will learn
  • Explain the iteration protocol (Symbol.iterator, next(), { value, done }) and make your own object iterable
  • Write generator functions with yield for lazy and infinite sequences
  • Use symbols as unique, non-colliding keys and recognise the well-known symbols

for...of works with arrays, strings, Map and Set, but not with a plain {} object — you get TypeError: ... is not iterable. What do arrays and strings have that objects lack? A special method under the key Symbol.iterator. Once you know this protocol, you can teach any object to work with for...of, spread and destructuring — and generators make it almost effortless.

Symbols

Symbol is a primitive type added in ES2015 (JavaScript has seven primitive types in total). Every call to Symbol() creates a unique value — even if the description is the same. Symbols are used as property keys that never clash with other keys: if a library adds its own symbol key to your object, it will not touch your id or name property. Symbol keys are hidden from for...in, Object.keys and JSON.stringify. Symbol.for('key') returns a symbol shared by the whole program.

JavaScript
const id1 = Symbol('id');
const id2 = Symbol('id');

console.log(typeof id1, id1 === id2);
console.log(id1.toString(), id1.description);

const user = { name: 'Leyla', [id1]: 501 };
console.log(user[id1]);
console.log(Object.keys(user), JSON.stringify(user));

console.log(Symbol.for('app') === Symbol.for('app'));
▸ Expected output
symbol false
Symbol(id) id
501
[ 'name' ] {"name":"Leyla"}
true
Well-known symbolWhat it controls
Symbol.iteratorfor...of, spread, destructuring
Symbol.asyncIteratorfor await...of
Symbol.toPrimitivehow an object turns into a number or string
Symbol.toStringTagthe name in the result of Object.prototype.toString

The iteration protocol

An object is iterable if it has a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a next() method; each call to next() returns { value, done }. done: true says the sequence has ended. for...of, spread ..., destructuring, Array.from, new Map(...) and Promise.all all use this protocol.

JavaScript
const letters = ['a', 'b'];
const iterator = letters[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
▸ Expected output
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true }
Behind the scenes for...of does exactly this: it calls next() until done: true arrives.
JavaScript
const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

for (const n of range) console.log(n);
console.log([...range], Math.max(...range));
const [first, second] = range;
console.log(first, second);
▸ Expected output
1
2
3
4
5
[ 1, 2, 3, 4, 5 ] 5
1 2

Generators

Writing an iterator by hand is long work. A generator function (function*) does not run its body when called; it returns a generator object, which is both an iterator and an iterable. yield hands out a value and pauses the function; the next next() resumes it right after the yield. The code runs lazily: a value is computed only when it is asked for.

JavaScript
function* countdown(from) {
  console.log('start');
  while (from > 0) {
    yield from;
    from--;
  }
  return 'liftoff';
}

const gen = countdown(3);
console.log('generator created');
console.log(gen.next());
console.log(gen.next().value, gen.next().value);
console.log(gen.next());
console.log([...countdown(2)]);
▸ Expected output
generator created
start
{ value: 3, done: false }
2 1
{ value: 'liftoff', done: true }
start
[ 2, 1 ]
“start” is printed only at the first next(). The return value arrives only together with done: true — for...of and spread ignore it.

Because values are computed on demand, a generator can be infinite: while (true) is safe here, since take pulls only as many values as needed and stops. This is also handy for reading big files line by line or data that arrives page by page.

JavaScript
function* ids() {
  let n = 1;
  while (true) yield `id-${n++}`;
}

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

function* take(iterable, count) {
  if (count <= 0) return;
  for (const item of iterable) {
    yield item;
    if (--count === 0) return;
  }
}

console.log([...take(ids(), 3)]);
console.log([...take(fibonacci(), 10)].join(' '));
▸ Expected output
[ 'id-1', 'id-2', 'id-3' ]
0 1 1 2 3 5 8 13 21 34

Generators in practice

JavaScript
class Playlist {
  #songs = [];

  add(...titles) {
    this.#songs.push(...titles);
    return this;
  }

  *[Symbol.iterator]() {
    yield* this.#songs;
  }
}

const playlist = new Playlist().add('Morning', 'Caspian Waves', 'Old Baku');

for (const [index, song] of [...playlist].entries()) {
  console.log(`${index + 1}. ${song}`);
}
console.log(Array.from(playlist, (song) => song.length));
▸ Expected output
1. Morning
2. Caspian Waves
3. Old Baku
[ 7, 13, 8 ]
yield* delegates to another iterable — here the private array. The songs stay private, yet the class works with for...of and Array.from.

When data arrives over time (pages of an API, chat messages), an async generator (async function*) and the for await...of loop come in handy: the loop waits for each next piece.

JavaScript
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function* loadPages(total) {
  for (let page = 1; page <= total; page++) {
    await wait(100);
    yield [`item ${page * 2 - 1}`, `item ${page * 2}`];
  }
}

for await (const items of loadPages(3)) {
  console.log(items);
}
console.log('All pages loaded');
▸ Expected output
[ 'item 1', 'item 2' ]
[ 'item 3', 'item 4' ]
[ 'item 5', 'item 6' ]
All pages loaded
Exercise

Write the generator chunk(array, size): it splits the array into pieces of size items and yields each piece (the last piece may be shorter).

Exercise · JavaScript
function* chunk(array, size) {
  // yield slices of the array with `size` items each
}

for (const part of chunk([1, 2, 3, 4, 5, 6, 7], 3)) {
  console.log(part);
}
console.log([...chunk(['a', 'b', 'c', 'd'], 2)].length);
▸ Expected output
[ 1, 2, 3 ]
[ 4, 5, 6 ]
[ 7 ]
2
Exercise

Make the countdown object iterable without a generator: its [Symbol.iterator]() method returns an object with a next() method that produces the numbers from start down to 1. Every loop must get a fresh iterator.

Exercise · JavaScript
const countdown = {
  start: 3,
  // add [Symbol.iterator]() that returns { next() { ... } }
};

console.log([...countdown]);
for (const n of countdown) {
  console.log(`T-${n}`);
}
▸ Expected output
[ 3, 2, 1 ]
T-3
T-2
T-1

Key points

  • A symbol is a unique primitive; symbol keys never clash and are hidden from Object.keys and JSON.stringify.
  • An iterable has a [Symbol.iterator]() method that returns an iterator; the iterator's next() returns { value, done }.
  • for...of, spread, destructuring and Array.from work with any iterable.
  • function* and yield create pausable, lazy — even infinite — generators.
  • async function* and for await...of are for data that arrives over time.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What is Symbol('x') === Symbol('x')?