AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

06 Collections and iteration / Lesson 3 of 5

The iteration protocol — what for…of, spread and destructuring actually call

13 min · +10 XP · 2 exercises · 5 questions

Read the text13 min
Exercises0/2
Quiz5 q

Quick answer

What is an iterator in JavaScript?

An iterator in JavaScript is an object with a next() method that returns { value, done }, one result per call, until done is true. An object is iterable when its [Symbol.iterator]() method returns such an iterator. That one contract is what for…of, spread, destructuring, Array.from, new Map() and Promise.all call, so any class that implements it works with all of them.

for…of, [...x], const [a, b] = x, Array.from(x), new Map(x), Promise.all(x), yield* — every one of these works on any object that follows one small contract, the iteration protocol. Arrays, strings, Maps, Sets, arguments, typed arrays, DOM lists and generator objects follow it; plain objects do not, which is why for (const x of {a: 1}) throws. Implement the contract on your own class and all of that syntax works on it for free. This lesson states the protocol precisely, shows a hand-written iterator, and covers the details that matter in practice: laziness, early exit, and what is and is not iterable.

Two interfaces

An object is iterable if it has a method under the key Symbol.iterator that returns an iterator. An iterator is any object with a next() method returning { value, done }:

const iterable = {
  from: 1, to: 3,
  [Symbol.iterator]() {                     // called once per loop
    let current = this.from, last = this.to;
    return {                                // the iterator — fresh state per loop
      next() {
        return current <= last ? { value: current++, done: false } : { value: undefined, done: true };
      },
    };
  },
};
[...iterable];                 // [1, 2, 3]
for (const n of iterable) { }  // 1, 2, 3
const [first] = iterable;      // 1
Math.max(...iterable);         // 3

for…of does exactly: call x[Symbol.iterator](), then call next() repeatedly, binding value until done is true. The value on the final done: true result is ignored by for…of and spread (it is the return value, visible only to yield* and manual callers). Because the iterator is created per loop, you can iterate the same iterable many times and each loop starts fresh — as long as [Symbol.iterator] returns a new iterator each call.

Iterators are often iterable too

The iterator objects the built-ins hand back (arr.values(), map.entries(), generator objects) also have a [Symbol.iterator] that returns this, so you can pass them straight to for…of — but they are single-use: once exhausted, a second loop sees nothing. const it = m.keys(); [...it]; [...it] gives the keys and then []. An iterable (the Map) restarts; an iterator (the result of keys()) does not.

Manual iteration

const it = "hi"[Symbol.iterator]();
it.next();   // { value: "h", done: false }
it.next();   // { value: "i", done: false }
it.next();   // { value: undefined, done: true }

Manual next() is how you pull some of a sequence — the first item and then the rest through a different path, or two iterators zipped in lockstep, or a parser that reads one token ahead. Everything higher-level is built from this.

What is iterable

Iterable: Array, String (by code point, so emoji are one item — module 10), Map, Set, arguments, typed arrays, NodeList/HTMLCollection in browsers, generator objects, the results of keys()/values()/entries(). Not iterable: plain objects (use Object.entries(obj) — an array of pairs, which is), numbers, null/undefined (spreading either into an array throws; spreading into an object does not). Array.from(x) accepts iterables and array-likes (objects with length and indices), which is why it works on { length: 3 } where spread does not.

Laziness and early exit

Values are produced on demand: next() computes the next one only when asked. An iterator over a file, a database cursor, or an infinite sequence is fine as long as consumers stop. for…of stops when you break, return or throw — and then calls the iterator's optional return() method so it can release resources (close the file, clear a timer). Destructuring a prefix (const [a, b] = it) also calls return(). Spread and Array.from consume everything — never spread an infinite iterator.

function* watched() { try { yield 1; yield 2; yield 3; } finally { console.log("cleanup"); } }
for (const v of watched()) { if (v === 2) break; }   // logs "cleanup" — break triggered return()

Iterables in your own classes

class Playlist {
  #tracks = [];
  add(t) { this.#tracks.push(t); return this; }
  [Symbol.iterator]() { return this.#tracks[Symbol.iterator](); }    // delegate to the array's iterator
  *shuffled() { /* a generator method — next lesson */ }
}
for (const t of new Playlist().add("a").add("b")) { }

Delegating to an inner array's iterator is the one-liner; when the sequence is computed (a tree walk, a range, a filtered view) write the iterator by hand or, far more readably, as a generator — the whole next lesson. Exposing iteration rather than the underlying array keeps the array private and lets you change the storage later.

Consumers you already use

Array.from(it, mapFn), new Set(it), new Map(pairs), Object.fromEntries(pairs), Promise.all(it), Math.max(...it), fn(...it), [...it], const [a, ...rest] = it, yield* it, for await (const x of asyncIt). Node 22's iterator helpers (it.map().filter().take()) do not exist on Node 16 — write generators for the same effect.

Common mistakes

  • Returning this from [Symbol.iterator] with state stored on the iterable — the second loop finds it exhausted.
  • Spreading a plain object into an array, or a Map into Object.fromEntries without checking the keys are strings.
  • Forgetting that String iterates by code point while .length and indexing are by UTF-16 unit.
  • Spreading a possibly-infinite or huge iterator.
  • Holding an iterator across break without letting return() run (only a problem when you call next() by hand — then call it.return?.() yourself).

Interview angle

  • "What makes an object iterable?" A [Symbol.iterator]() method returning an object with next() → { value, done }.
  • "Iterable versus iterator?" Iterable produces iterators (restartable); an iterator is the cursor (single-use); built-in iterators are also iterable by returning themselves.
  • "Why can't you for…of an object?" No Symbol.iterator; use Object.entries.
  • "What happens on break inside for…of?" The iterator's return() is called so it can clean up.
  • "Make a class work with spread and for…of." Implement [Symbol.iterator], usually by delegating or with a generator.

Key takeaways

  • Iterable = has [Symbol.iterator](); iterator = has next() returning { value, done }; the protocol is what for…of, spread, destructuring and Array.from call.
  • Return a fresh iterator per call so loops restart; built-in iterators are single-use.
  • Strings iterate by code point; plain objects are not iterable; Array.from also takes array-likes.
  • Iteration is lazy; break calls return(); never spread the infinite.
  • Own classes: delegate to an inner iterator or write a generator.

Common questions

What is the difference between an iterable and an iterator?

An iterable produces iterators: its [Symbol.iterator]() returns a fresh one on each call, so it can be looped over again and again. An iterator is the cursor itself, with next(), and is single-use. Built-in iterators such as map.keys() are also iterable by returning themselves.

Why can't you use for…of on a plain object?

A plain object has no Symbol.iterator method, so for…of throws a TypeError saying it is not iterable. Loop over Object.entries(obj), Object.keys(obj) or Object.values(obj) instead — each returns an array, which is iterable.

What happens when you break out of a for…of loop?

The loop stops pulling values and calls the iterator's optional return() method, so it can release resources such as a file handle or a timer. In a generator this runs its finally blocks. Destructuring only a prefix of an iterable calls return() too.

How do you make a class iterable in JavaScript?

Give it a [Symbol.iterator]() method that returns an iterator. When the class keeps its items in an array, delegate: return this.#items[Symbol.iterator](). When the sequence is computed, such as a tree walk or a range, write the method as a generator, *[Symbol.iterator]() { … }.

What is the difference between Array.from and spread?

Spread ([...x]) accepts only iterables. Array.from(x) accepts iterables and array-likes — objects with a length and numeric indices — so Array.from({ length: 3 }) works where spreading the same object throws. It also takes a mapping function as its second argument.

Mark as readSign in to keep your place.

JavaScript Iterators Explained: Iterable vs Iterator

A JavaScript object is iterable when Symbol.iterator returns an iterator with next(). What for…of, spread and destructuring call, and what break does.

  • Course: JavaScript study plan
  • Module: Collections and iteration
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: Node 16

What is an iterator in JavaScript?

An iterator in JavaScript is an object with a next() method that returns { value, done }, one result per call, until done is true. An object is iterable when its [Symbol.iterator]() method returns such an iterator. That one contract is what for…of, spread, destructuring, Array.from, new Map() and Promise.all call, so any class that implements it works with all of them.

Lesson

for…of, [...x], const [a, b] = x, Array.from(x), new Map(x), Promise.all(x), yield* — every one of these works on any object that follows one small contract, the iteration protocol. Arrays, strings, Maps, Sets, arguments, typed arrays, DOM lists and generator objects follow it; plain objects do not, which is why for (const x of {a: 1}) throws. Implement the contract on your own class and all of that syntax works on it for free. This lesson states the protocol precisely, shows a hand-written iterator, and covers the details that matter in practice: laziness, early exit, and what is and is not iterable.

Two interfaces

An object is iterable if it has a method under the key Symbol.iterator that returns an iterator. An iterator is any object with a next() method returning { value, done }:

const iterable = {
  from: 1, to: 3,
  [Symbol.iterator]() {                     // called once per loop
    let current = this.from, last = this.to;
    return {                                // the iterator — fresh state per loop
      next() {
        return current <= last ? { value: current++, done: false } : { value: undefined, done: true };
      },
    };
  },
};
[...iterable];                 // [1, 2, 3]
for (const n of iterable) { }  // 1, 2, 3
const [first] = iterable;      // 1
Math.max(...iterable);         // 3

for…of does exactly: call x[Symbol.iterator](), then call next() repeatedly, binding value until done is true. The value on the final done: true result is ignored by for…of and spread (it is the return value, visible only to yield* and manual callers). Because the iterator is created per loop, you can iterate the same iterable many times and each loop starts fresh — as long as [Symbol.iterator] returns a new iterator each call.

Iterators are often iterable too

The iterator objects the built-ins hand back (arr.values(), map.entries(), generator objects) also have a [Symbol.iterator] that returns this, so you can pass them straight to for…of — but they are single-use: once exhausted, a second loop sees nothing. const it = m.keys(); [...it]; [...it] gives the keys and then []. An iterable (the Map) restarts; an iterator (the result of keys()) does not.

Manual iteration

const it = "hi"[Symbol.iterator]();
it.next();   // { value: "h", done: false }
it.next();   // { value: "i", done: false }
it.next();   // { value: undefined, done: true }

Manual next() is how you pull some of a sequence — the first item and then the rest through a different path, or two iterators zipped in lockstep, or a parser that reads one token ahead. Everything higher-level is built from this.

What is iterable

Iterable: Array, String (by code point, so emoji are one item — module 10), Map, Set, arguments, typed arrays, NodeList/HTMLCollection in browsers, generator objects, the results of keys()/values()/entries(). Not iterable: plain objects (use Object.entries(obj) — an array of pairs, which is), numbers, null/undefined (spreading either into an array throws; spreading into an object does not). Array.from(x) accepts iterables and array-likes (objects with length and indices), which is why it works on { length: 3 } where spread does not.

Laziness and early exit

Values are produced on demand: next() computes the next one only when asked. An iterator over a file, a database cursor, or an infinite sequence is fine as long as consumers stop. for…of stops when you break, return or throw — and then calls the iterator's optional return() method so it can release resources (close the file, clear a timer). Destructuring a prefix (const [a, b] = it) also calls return(). Spread and Array.from consume everything — never spread an infinite iterator.

function* watched() { try { yield 1; yield 2; yield 3; } finally { console.log("cleanup"); } }
for (const v of watched()) { if (v === 2) break; }   // logs "cleanup" — break triggered return()

Iterables in your own classes

class Playlist {
  #tracks = [];
  add(t) { this.#tracks.push(t); return this; }
  [Symbol.iterator]() { return this.#tracks[Symbol.iterator](); }    // delegate to the array's iterator
  *shuffled() { /* a generator method — next lesson */ }
}
for (const t of new Playlist().add("a").add("b")) { }

Delegating to an inner array's iterator is the one-liner; when the sequence is computed (a tree walk, a range, a filtered view) write the iterator by hand or, far more readably, as a generator — the whole next lesson. Exposing iteration rather than the underlying array keeps the array private and lets you change the storage later.

Consumers you already use

Array.from(it, mapFn), new Set(it), new Map(pairs), Object.fromEntries(pairs), Promise.all(it), Math.max(...it), fn(...it), [...it], const [a, ...rest] = it, yield* it, for await (const x of asyncIt). Node 22's iterator helpers (it.map().filter().take()) do not exist on Node 16 — write generators for the same effect.

Common mistakes

  • Returning this from [Symbol.iterator] with state stored on the iterable — the second loop finds it exhausted.
  • Spreading a plain object into an array, or a Map into Object.fromEntries without checking the keys are strings.
  • Forgetting that String iterates by code point while .length and indexing are by UTF-16 unit.
  • Spreading a possibly-infinite or huge iterator.
  • Holding an iterator across break without letting return() run (only a problem when you call next() by hand — then call it.return?.() yourself).

Interview angle

  • "What makes an object iterable?" A [Symbol.iterator]() method returning an object with next() → { value, done }.
  • "Iterable versus iterator?" Iterable produces iterators (restartable); an iterator is the cursor (single-use); built-in iterators are also iterable by returning themselves.
  • "Why can't you for…of an object?" No Symbol.iterator; use Object.entries.
  • "What happens on break inside for…of?" The iterator's return() is called so it can clean up.
  • "Make a class work with spread and for…of." Implement [Symbol.iterator], usually by delegating or with a generator.

Key takeaways

  • Iterable = has [Symbol.iterator](); iterator = has next() returning { value, done }; the protocol is what for…of, spread, destructuring and Array.from call.
  • Return a fresh iterator per call so loops restart; built-in iterators are single-use.
  • Strings iterate by code point; plain objects are not iterable; Array.from also takes array-likes.
  • Iteration is lazy; break calls return(); never spread the infinite.
  • Own classes: delegate to an inner iterator or write a generator.

Common questions

What is the difference between an iterable and an iterator?

An iterable produces iterators: its [Symbol.iterator]() returns a fresh one on each call, so it can be looped over again and again. An iterator is the cursor itself, with next(), and is single-use. Built-in iterators such as map.keys() are also iterable by returning themselves.

Why can't you use for…of on a plain object?

A plain object has no Symbol.iterator method, so for…of throws a TypeError saying it is not iterable. Loop over Object.entries(obj), Object.keys(obj) or Object.values(obj) instead — each returns an array, which is iterable.

What happens when you break out of a for…of loop?

The loop stops pulling values and calls the iterator's optional return() method, so it can release resources such as a file handle or a timer. In a generator this runs its finally blocks. Destructuring only a prefix of an iterable calls return() too.

How do you make a class iterable in JavaScript?

Give it a [Symbol.iterator]() method that returns an iterator. When the class keeps its items in an array, delegate: return this.#items[Symbol.iterator](). When the sequence is computed, such as a tree walk or a range, write the method as a generator, *[Symbol.iterator]() { … }.

What is the difference between Array.from and spread?

Spread ([...x]) accepts only iterables. Array.from(x) accepts iterables and array-likes — objects with a length and numeric indices — so Array.from({ length: 3 }) works where spreading the same object throws. It also takes a mapping function as its second argument.

Exercises

A Range that speaks the protocol

Give Range(from, to, step) a hand-written [Symbol.iterator]() returning an iterator object with next() (values from, from+step, … while ≤ to) and return(), which increments the global returnCalls. Each call must return a fresh iterator so the range can be looped repeatedly. Line 1 is from to step, line 2 a value to break at. Print spread=<[...r]>, then sum=<via for…of> again=<[...r] a second time>, then first=<> second=<> squares=<Array.from(r, x => x*x)> (destructure the first two), then run for…of breaking at the given value and print breakAt=<v> returnCalled=<how many times return() ran during that loop>.

Example: 1 10 3 then 4 →

spread=[1,4,7,10]
sum=22 again=[1,4,7,10]
first=1 second=4 squares=[1,16,49,100]
breakAt=4 returnCalled=1

What is iterable?

Each line is a JSON value. Decide whether it is iterable (typeof value[Symbol.iterator] === "function", guarding null). For an iterable print <line>: iterable items=<[...value].length> first=<JSON of the first item>; for a non-iterable object print <line>: not iterable entries=<Object.entries(value).length>; for anything else <line>: not iterable (<null or typeof>).

Example:

[1,2,3]
"abc"
{"a":1,"b":2}
42
null

→

[1,2,3]: iterable items=3 first=1
"abc": iterable items=3 first="a"
{"a":1,"b":2}: not iterable entries=2
42: not iterable (number)
null: not iterable (null)

In this module: Collections and iteration

← WeakMap, WeakSet and WeakRef — data that does not keep objects alive · Generators — functions that pause, and lazy pipelines →