Lesson 44 of 80
for...of, spread, and destructuring all work on arrays for the same reason — they all call one specific method. Give your own objects that method and they get the same powers.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
for...of, the spread operator, Array.from, and array destructuring all look like separate JavaScript features, but they're really one feature wearing four costumes: they all work on anything that has a method at the well-known key Symbol.iterator. Arrays, strings, Maps, and Sets all have one built in. Plain objects don't — which is exactly why for (const x of {a: 1}) throws TypeError: {} is not iterable, while for (const x of [1, 2, 3]) doesn't.
Implementing Symbol.iterator yourself means writing a method that returns an iterator: an object with a .next() method that returns { value, done } each time it's called. Once your object has that method, every language feature that consumes iterables — for...of, spread, destructuring, Array.from, Promise.all — just works on it, for free.
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) return { value: undefined, done: true };
const value = current;
current += step;
return { value, done: false };
},
};
}
}
const evens = new Range(0, 10, 2);
for (const n of evens) {
console.log(n); // 0, 2, 4, 6, 8, 10
}
[...evens]; // [0, 2, 4, 6, 8, 10]
const [first, second] = evens; // first = 0, second = 2
Array.from(evens); // [0, 2, 4, 6, 8, 10]
Range isn't an array — it never stores [0, 2, 4, ...] anywhere. It stores three numbers and computes the next value on demand. for...of calls evens[Symbol.iterator]() once to get the iterator object, then calls .next() repeatedly until done is true. Every consumer above does the exact same protocol — the syntax differs, the mechanism underneath doesn't.
Note that evens is iterated twice above (once in the loop, once by spread) and both runs start from 0 again. That's because [Symbol.iterator]() is called fresh each time and returns a brand-new iterator with its own current closed over — the Range object itself holds no iteration position.
[Symbol.iterator](), a method that returns the iterator. The iterator has .next(). Conflating them is the most common bug — writing .next() directly on the object you're trying to loop over instead of on what its [Symbol.iterator]() returns.{done: true} should still return a value key (or omit it), never skip the shape. Consumers check done first, but a malformed return like a bare undefined instead of { value: undefined, done: true } breaks destructuring and spread, which expect the full shape every call..length still isn't iterable. {0: 'a', 1: 'b', length: 2} looks array-like but has no Symbol.iterator; for...of still throws on it. That's the actual distinction between "array-like" and "iterable" — Array.from accepts both, but for...of and spread only accept the latter.A generator function is the language's shortcut for exactly this pattern — every yield is one .next() call handled automatically, so function* bodies never have to construct the {value, done} object by hand the way Range does above. If you understand generators, you already understand what Symbol.iterator is built to satisfy; this lesson is what's underneath that shortcut.