ESC

Type to search the knowledge base.

Iterators and the Iterable Protocol

Symbol.iterator, next(), and for...of — how iterables work, custom iterators, and the difference from arrays.

intermediate3 min read
  • javascript
  • iterators
  • iterable
  • symbol

for...of, spread, and destructuring all speak the iterable protocol. An object is iterable if it has a Symbol.iterator method that returns an iterator. An iterator has next() → { value, done }. Arrays, Maps, Sets, strings, and NodeLists (modern) implement this. Knowing the protocol lets you build lazy collections and understand generators.

The two protocols

const arr = [10, 20];
const iterator = arr[Symbol.iterator]();

iterator.next(); // { value: 10, done: false }
iterator.next(); // { value: 20, done: false }
iterator.next(); // { value: undefined, done: true }
// iterable: has Symbol.iterator
// iterator: has next()
// many iterators are also iterable (return self) so for...of works twice carefully

for…of under the hood

for (const x of iterable) {
  // ...
}

// roughly:
const it = iterable[Symbol.iterator]();
let r = it.next();
while (!r.done) {
  const x = r.value;
  // body
  r = it.next();
}
// if break/throw: it.return?.()

for...in is different — it walks keys (enumerable props), not iterator values.

Custom iterable

const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let n = this.from;
    const last = this.to;
    return {
      next() {
        if (n <= last) return { value: n++, done: false };
        return { value: undefined, done: true };
      },
    };
  },
};

[...range]; // [1, 2, 3]

With a generator (cleaner):

const range2 = {
  from: 1,
  to: 3,
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) yield n;
  },
};

Built-in iterables

for (const ch of 'hi') {
  /* h, i */
}
for (const [k, v] of map) {
  /* ... */
}
for (const v of set) {
  /* ... */
}
for (const node of document.querySelectorAll('div')) {
  /* NodeList is iterable */
}

Objects are not iterable by default:

// for (const x of { a: 1 }) // TypeError
for (const [k, v] of Object.entries({ a: 1 })) {
  /* ... */
}

Iterator helpers (modern)

// stage / shipping in newer engines — map/filter on iterators
// Iterator.from(obj).map(fn).take(5).toArray()

Check support before relying on them; generators + functions still work everywhere.

Infinite iterables

function* forever() {
  let i = 0;
  for (;;) yield i++;
}

// never spread infinite iterables
// [...forever()] // 💥

Consume with break, take, or manual next.

return() and closing

const it = {
  [Symbol.iterator]() {
    return this;
  },
  next() {
    return { value: 1, done: false };
  },
  return() {
    console.log('cleaned');
    return { done: true };
  },
};

for (const x of it) {
  break; // logs cleaned
}

Implement return when your iterator holds resources.

Interview answer

“An iterable implements Symbol.iterator returning an iterator with next() that yields {value, done}. for…of, spread, and array destructuring use that protocol. Arrays, Maps, Sets, and strings are iterable; plain objects are not. Generators make custom iterables easy. for…in is unrelated—it enumerates keys.”

Consuming partial iterators

function first(iterable) {
  const it = iterable[Symbol.iterator]();
  const { value, done } = it.next();
  it.return?.(); // close if we stop early
  return done ? undefined : value;
}

function* take(n, iterable) {
  let i = 0;
  for (const x of iterable) {
    yield x;
    if (++i >= n) break; // triggers return() on generator iterators
  }
}

Early exit should close resources — for...of does this on break. Manual next() loops should call return() when abandoning an iterator that holds handles. That’s part of being a good citizen of the protocol, not just implementing next.

Further reading

Related guides