How do you make instances of your own JavaScript class work with for...of and array spread, without using a generator function?
answer
- one method, computed key
- brackets around the symbol
- instance method, never static
- returns a fresh cursor object
- state in a local, not a field
basics
~20 sAdd one method to the class, keyed with the computed name [Symbol.iterator], that returns a fresh iterator object each call — an object whose next() returns { value, done } and whose loop state lives in local variables.
solid answer
~50 sYou define a single method under the computed key `[Symbol.iterator]()` in the class body. It must return an iterator: an object with a `next()` method that returns `{ value, done }`, reporting `done: true` when the sequence is over. The important detail is *where the cursor lives* — declare it as a local variable inside the method, not as an instance field, so every call hands back an independent iterator. That is what lets the same instance be looped twice, or nested inside itself, exactly like an array. Because the method key must be the symbol rather than the string, it has to be written with brackets, and it must be an instance method, not a `static` one. Once it is there, the instance works with `for...of`, `[...instance]`, array destructuring, `Array.from`, and the `Set` and `Map` constructors — every consumer in the language, from one method.
code
javascript · 27 linesclass Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let n = this.start;
const { end, step } = this;
return {
next() {
if (n >= end) return { value: undefined, done: true };
const value = n;
n += step;
return { value, done: false };
},
[Symbol.iterator]() { return this; }
};
}
}
const r = new Range(0, 5, 2);
console.log([...r]); // [0, 2, 4]
console.log([...r]); // [0, 2, 4] again — fresh cursor
const [first, second] = new Range(10, 20);
console.log(first, second); // 10 11go deeper
Remember that a class needs a method written as Symbol.iterator returning an object with next(), and that next() must return { value, done } rather than the element itself.
Explain why the cursor belongs in a local variable inside the method: a fresh iterator per call is what allows repeated and nested loops. Know that the method must be an instance method, and that delegating to an internal array is the safest implementation.
Judge whether exposing a type as iterable is the right public contract at all, and make the implementation robust — fresh cursors, an iterable iterator, no reliance on how next() is called.
Own the API-shape decision: an iterable contract lets consumers stream and compose without knowing your type, but it also commits you to element order and to whatever laziness callers come to depend on. Decide when a plain array getter is the safer promise.
## The one method you have to write A class becomes iterable when its instances can answer `instance[Symbol.iterator]()` with an iterator. In a class body that is written with a computed key: ```js class Range { constructor(start, end, step = 1) { this.start = start; this.end = end; this.step = step; } [Symbol.iterator]() { let n = this.start; // cursor: local to this call const { end, step } = this; return { next() { if (n >= end) return { value: undefined, done: true }; const value = n; n += step; return { value, done: false }; } }; } } [...new Range(0, 5, 2)]; // [0, 2, 4] ``` Three syntactic details trip people up: - The brackets are mandatory. `Symbol.iterator() {}` would define a method literally named `"Symbol"`… no, worse: it is a syntax error in that shape, and `"Symbol.iterator"() {}` defines a string-keyed method the protocol never looks at. Only the computed form reaches the well-known symbol. - It must not be `static`. A static method lives on the constructor function, so `for (const x of Range)` would work and `for (const x of new Range(...))` would not — the opposite of what you want. - The method may live on a base class; lookup goes through the prototype chain like any other method, so subclasses inherit iterability for free. ## Where the state must live The returned object holds the traversal position. Put it in a local variable captured by the closure, as above, and each call to `[Symbol.iterator]()` produces a brand-new cursor starting at the beginning. That is what makes this work: ```js const r = new Range(0, 3); [...r]; // [0, 1, 2] [...r]; // [0, 1, 2] — a second, independent pass for (const a of r) for (const b of r) { /* 9 pairs, both loops correct */ } ``` If instead you store the cursor as an instance field — `this.i = 0` in the constructor, incremented by `next()` — the instance itself becomes the cursor. The first loop drains it, the second sees nothing, and a nested loop over the same instance breaks the outer one. That is a genuine bug class, and it is why built-in collections return a fresh iterator per call. ## Making the iterator iterable too A bare `{ next() {} }` object satisfies the iterator protocol but not the iterable one, which matters if a caller ever holds on to the iterator and tries to `for...of` it directly. The conventional fix is one extra line: ```js return { next() { /* ... */ }, [Symbol.iterator]() { return this; } }; ``` Built-in iterators get this behaviour from `%IteratorPrototype%`, whose `[Symbol.iterator]` returns `this`, which is why `for (const v of map.values())` works. ## Beware `this` inside next() In the example above, `next()` reads only closure variables, so it does not matter how it is called. If you write `next()` to read `this.something`, you are relying on the consumer calling it as `it.next()` — true in practice, but fragile if the method is ever extracted. Either keep everything in the closure or use an arrow function so `this` stays lexical. ## The cheapest implementation of all When your class already wraps something iterable, delegate instead of hand-rolling: ```js class Playlist { #tracks = []; add(t) { this.#tracks.push(t); return this; } [Symbol.iterator]() { return this.#tracks[Symbol.iterator](); } } ``` The array's own iterator already provides fresh-cursor semantics, correct result records and iterable-iterator behaviour, so there is nothing left to get wrong. Delegation also keeps the backing store private: consumers get the elements without a reference to the array itself. ## What you gain The payoff is that iterability is a *language-wide* contract, not a per-consumer one. The one method simultaneously enables `for...of`, `[...instance]`, `f(...instance)`, `const [a, b] = instance`, `Array.from(instance)`, `new Set(instance)`, `new Map(instance)` when the elements are pairs, and `Promise.all(instance)` when they are promises. No consumer needs to know your type exists — which is precisely the point of standardising on a protocol rather than a named method like `toArray()`.
- What breaks if you keep the iteration index as an instance property instead of a local variable?The instance becomes a single shared cursor. The first for...of drains it, a second loop over the same instance yields nothing, and a nested loop over it corrupts the outer pass. Built-in collections avoid this by returning a fresh iterator from every Symbol.iterator call.
- Does marking the method static change anything?Yes, and wrongly. A static method lives on the constructor, so the class object itself would become iterable while instances stay non-iterable and throw on for...of. The iterator hook must be an instance method, inherited through the prototype chain.
- Why do people add [Symbol.iterator]() { return this; } to the returned iterator?So the iterator is also iterable. A bare object with only next() cannot be passed to for...of or spread directly. Returning `this` satisfies the iterable protocol, which is exactly what built-in iterators inherit from %IteratorPrototype%.
- Is delegating to an internal array's iterator good enough?Usually the best option. `return this.#items[Symbol.iterator]()` inherits fresh-cursor semantics, well-formed result records and iterable-iterator behaviour with no code to get wrong, while keeping the backing array private from consumers.
saying these in an interview costs you the question
- Defines a plain next() method on the class itself
- Writes the method key as the string 'Symbol.iterator'
- Marks the iterator method static
- Stores the cursor in the constructor as this.index
- Returns the value directly instead of a result object