JavaScript Interview Prep
ES6+ Features

Iterators and Iterables

The Protocol Behind for...of

LinkedIn Hook

You write for (const x of myObj) { ... } and JavaScript throws TypeError: myObj is not iterable.

Why? Because plain objects don't implement the iterator protocol. Arrays, strings, Sets, Maps, NodeLists, and arguments all do — and they all share the same contract: an object that produces an iterator when you call [Symbol.iterator]().

The iterator protocol is the quiet standard behind for...of, spread (...), destructuring, Array.from, Promise.all, and every generator in existence. Implement it once on a custom class and every consumer "just works."

In this lesson I unpack the two protocols (iterable + iterator), build a Range class from scratch, and walk through everything that consumes iterables under the hood.

If you've ever copy-pasted Array.from(...) without knowing why it works — this lesson is the answer.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #ES6 #Iterators #ForOf #Symbol #CodingInterview


What You'll Learn

  • The two protocols: iterable (has [Symbol.iterator]()) and iterator (has next() -> { value, done })
  • How to make a custom class work with for...of, spread, destructuring, and Array.from
  • Every language construct that silently calls [Symbol.iterator]() on your behalf

The Bookmark Metaphor

An iterator is like a bookmark in a book. Each time you say "next page," it gives you the current page and moves forward. When you reach the end, it says "done." An iterable is the book itself — anything that knows how to produce a bookmark.

The Iterator Protocol

An object is an iterator if it has a next() method that returns { value, done }:

// Manual iterator
function createCountdown(start) {
    let current = start;
    return {
        next() {
            if (current > 0) {
                return { value: current--, done: false };
            }
            return { value: undefined, done: true };
        },
    };
}

const countdown = createCountdown(3);
countdown.next(); // { value: 3, done: false }
countdown.next(); // { value: 2, done: false }
countdown.next(); // { value: 1, done: false }
countdown.next(); // { value: undefined, done: true }

The Iterable Protocol — Symbol.iterator

An object is iterable if it has a [Symbol.iterator]() method that returns an iterator:

// Built-in iterables
const arr = [10, 20, 30];
const iter = arr[Symbol.iterator]();
iter.next(); // { value: 10, done: false }
iter.next(); // { value: 20, done: false }
iter.next(); // { value: 30, done: false }
iter.next(); // { value: undefined, done: true }

// Strings are iterable
const strIter = "Hi"[Symbol.iterator]();
strIter.next(); // { value: "H", done: false }
strIter.next(); // { value: "i", done: false }

Custom Iterable — Range Class

class Range {
    constructor(start, end, step = 1) {
        this.start = start;
        this.end = end;
        this.step = step;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const { end, step } = this;

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

// Now Range works with for...of, spread, destructuring
const range = new Range(1, 5);

for (const n of range) {
    console.log(n); // 1, 2, 3, 4, 5
}

console.log([...range]); // [1, 2, 3, 4, 5]
const [first, second] = range; // first = 1, second = 2
console.log(Array.from(range)); // [1, 2, 3, 4, 5]

// With step
const evens = new Range(0, 10, 2);
console.log([...evens]); // [0, 2, 4, 6, 8, 10]

for...of Consuming Iterators

// for...of works with any iterable
for (const char of "Hello") {
    console.log(char); // H, e, l, l, o
}

for (const [key, value] of new Map([
    ["a", 1],
    ["b", 2],
])) {
    console.log(key, value); // a 1, b 2
}

for (const item of new Set([1, 2, 3])) {
    console.log(item); // 1, 2, 3
}

// Plain objects are NOT iterable by default
// for (const x of { a: 1 }) {} // TypeError: not iterable

Spread with Iterables

// Spread works with any iterable
const str = "Hello";
const chars = [...str]; // ["H", "e", "l", "l", "o"]

const set = new Set([1, 2, 3]);
const arr = [...set]; // [1, 2, 3]

// Combine iterables
const combined = [...new Range(1, 3), ...new Range(7, 9)];
// [1, 2, 3, 7, 8, 9]

Common Mistakes

  • Expecting plain objects to work with for...of — they don't. Only iterables do. If you need key/value iteration, call Object.entries(obj) which returns an iterable array.
  • Returning an iterator from [Symbol.iterator]() that doesn't itself implement [Symbol.iterator]() (and therefore can't be used with a second for...of). Add [Symbol.iterator]() { return this; } to the iterator object so it is also an iterable.
  • Mutating the underlying collection while iterating — the iterator may produce stale values, skip elements, or visit the same element twice depending on the collection type. Snapshot the data first if you plan to mutate.

Interview Questions

Q: What is the difference between an iterator and an iterable?

An iterable is an object with a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a next() method that returns { value, done }. Arrays, strings, Maps, and Sets are built-in iterables. Plain objects are NOT iterable by default.

Q: How would you make a custom object iterable?

Implement [Symbol.iterator]() on the object (or its class). This method must return an object with a next() method that returns { value, done }. Once implemented, the object works with for...of, spread, destructuring, and Array.from.

Q: What consumes iterables in JavaScript?

for...of loops, spread operator (...), destructuring assignment, Array.from(), Promise.all(), Map and Set constructors, yield* in generators, and any API that accepts an iterable.

Q: What is Symbol.iterator?

Symbol.iterator is a well-known symbol whose value is the key used by the iterable protocol. Any object with a [Symbol.iterator]() method is iterable. Using a symbol (rather than a string key) prevents name collisions with regular properties.


Quick Reference — Cheat Sheet

ITERATORS & ITERABLES — QUICK MAP

Protocols:
  Iterable    -> obj[Symbol.iterator]() returns an Iterator
  Iterator    -> obj.next() returns { value, done }

Built-in iterables:
  Array, String, Map, Set, NodeList, arguments,
  TypedArray, generator return values.

NOT iterable by default:
  Plain objects {} — use Object.entries(obj) for kv iteration.

Consumers (each calls [Symbol.iterator]() under the hood):
  for...of, spread (...), destructuring, Array.from(),
  Promise.all / Promise.race / Promise.any,
  new Map(entries), new Set(values), yield* gen

Minimal custom iterable:
  class X {
    [Symbol.iterator]() {
      let i = 0;
      return {
        next() {
          return i < 3
            ? { value: i++, done: false }
            : { value: undefined, done: true };
        }
      };
    }
  }

Previous: Modules -> import, export, and Dynamic Loading Next: Set, Map, WeakSet, WeakMap -> Purpose-Built Collections


This is Lesson 9.6 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page