JAVASCRIPT / DESTRUCTURING, ITERABLES, AND GENERATORS
Making your own iterable with Symbol.iterator
Write a [Symbol.iterator] method so your own objects and classes work with for...of, spread, and destructuring, and stay reusable.
What you will learn
- Define [Symbol.iterator]() on an object or class to make it work with for...of
- Return an iterator: an object whose next() answers with { value, done }
- Keep the cursor in a let inside [Symbol.iterator] so every pass restarts
- Add return() to the iterator to clean up when a loop exits early with break
Understanding Making your own iterable with Symbol.iterator
Two different objects are involved whenever for...of runs. The iterable is the thing you loop over, and its only job is to own a method stored under the key Symbol.iterator; the iterator is what that method returns, and its only job is to answer next() with a result object shaped { value, done }. for...of calls the factory method once, then calls next() repeatedly until a result comes back with done: true, at which point it stops and discards the value that came with it.
The key is a symbol rather than the string 'iterator' because the protocol has to be unambiguous: a property named iterator could plausibly be your own data, while Symbol.iterator is one well-known value nothing else can accidentally occupy. Being a symbol also keeps the hook out of for...in and JSON.stringify, and because property lookup walks the prototype chain, defining it as a class method makes every instance of that class iterable.
Where you keep the cursor decides whether your object can be walked more than once. Declare it with let inside [Symbol.iterator] and each call builds a private counter, so two loops or two spreads both see the whole sequence. Store the cursor on this and return this instead, and you have written a one-shot iterator: the first consumer drains it and later ones get done: true immediately. Both shapes are legal — arrays and Maps are reusable, generator objects are one-shot — so choose deliberately.
const range = {
from: 3,
to: 6,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) return { value: current++, done: false };
return { value: undefined, done: true };
}
};
}
};
for (const n of range) {
console.log(n);
}
console.log([...range].join(','));
console.log([...range].join(','));
console.log(Math.max(...range));An iterable is any object with a [Symbol.iterator] method that hands back a fresh iterator, and an iterator is any object whose next() returns { value, done }.
Worked examples
The one-shot trap
Shows what happens when the cursor lives on the object and [Symbol.iterator] returns this.
const counter = {
current: 1,
last: 3,
[Symbol.iterator]() {
return this;
},
next() {
return this.current <= this.last
? { value: this.current++, done: false }
: { done: true };
}
};
console.log([...counter].join(','));
console.log([...counter].length);
console.log(counter.current);Example explained
Line 1[Symbol.iterator]() { return this; } hands every consumer the same iterator instead of a new one.
Line 2this.current++ mutates the object, so the position survives after the first spread finishes.
Line 3The second spread calls next() once, gets done: true straight away, and produces an empty array.
Line 4counter.current is 4 at the end, which is the direct proof that nothing reset the cursor.
Borrowing an array's iterator
Makes a class iterable by delegating to the built-in iterator of an internal array.
class Playlist {
constructor(...tracks) {
this.tracks = tracks;
}
[Symbol.iterator]() {
return this.tracks[Symbol.iterator]();
}
}
const p = new Playlist('intro', 'verse', 'outro');
for (const track of p) {
console.log(track);
}
const [first, ...rest] = p;
console.log(first, rest.length);Example explained
Line 1this.tracks[Symbol.iterator]() returns the array's own iterator, so you never hand-write next().
Line 2The method sits on Playlist.prototype, and for...of finds it through the prototype chain.
Line 3Each call produces a brand new array iterator, so p can be looped any number of times.
Line 4Array destructuring reads the same hook, which is why [first, ...rest] works on the instance.
Cleanup with return()
Demonstrates the optional return() method that for...of calls when a loop is cut short by break.
class Countdown {
constructor(start) {
this.start = start;
}
[Symbol.iterator]() {
let n = this.start;
return {
next: () => (n > 0 ? { value: n--, done: false } : { done: true }),
return: () => {
console.log('stopped early at', n);
return { done: true };
}
};
}
}
for (const n of new Countdown(5)) {
if (n === 3) break;
console.log(n);
}
console.log('loop finished');Example explained
Line 1let n = this.start places the cursor inside the method, so each loop over a Countdown starts at the top.
Line 2next is an arrow function, so n stays reachable through the closure with no this to worry about.
Line 3break makes for...of call iterator.return() when that method exists, which is where you release resources.
Line 4next() had already produced 3 and lowered n to 2 before break ran, so the cleanup line reports 2.
Important notes
An iterator that never reports done: true will hang [...obj] and Array.from until the tab dies; consume such objects only with break or an explicit limit.
for...of ignores the value that ships alongside done: true, so the last real item must still come back with done: false.
Common mistakes
Using the plain name iterator or the string key 'Symbol.iterator' instead of the computed [Symbol.iterator]; for...of then throws 'obj is not iterable' because the real symbol key was never set.
Returning the bare value from next() instead of a result object, which fails with 'TypeError: Iterator result 3 is not an object' on the very first step.
Keeping the counter on this and returning this from [Symbol.iterator], so the first loop drains the object and every later loop, spread, or destructuring sees nothing.
Try it yourself
Change, predict, then run
In a browser editor build const evens = { limit: 10 } with a [Symbol.iterator] method that yields 0, 2, 4, 6, 8, 10, then log [...evens].join(',') twice to prove the object is reusable rather than one-shot.
Open the JavaScript workspaceCheck your understanding
An object stores its counter on itself and returns this from [Symbol.iterator](). The first for...of prints every value, but a second for...of over the same object prints nothing. Why?
- Returning this hands out one shared iterator that was already advanced past the end, so its next() immediately reports done: true
- for...of caches the result of [Symbol.iterator]() and never calls the method a second time
- Spreading or looping an object marks it non-iterable, so only the first consumer is allowed to walk it
- next() has to be defined on the prototype rather than on the object for repeated iteration to work
Show answer
The object is its own iterator, and its cursor sits on this, so the first loop leaves it exhausted and every later consumer receives that same finished iterator. Option 2 is tempting but wrong: for...of does call [Symbol.iterator]() again on the second loop — the problem is that the call returns the already-drained object instead of a new iterator. Moving the cursor into a let inside the method fixes it.