Iterators, generators, Map and Set
Symbol.iterator, generator functions and yield, lazy sequences, for...of, when a Map beats an object, and the Set operations you actually need.
The iterator protocol
An iterable has a method at Symbol.iterator returning an object with a next() method. for...of, spread, destructuring and Array.from all consume that interface, so implementing it is what makes your own object work with them.
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
[Symbol.iterator]() { return this; }, // an iterator is itself iterable
};
},
};
[...range]; // [1, 2, 3]
Array.from(range); // [1, 2, 3]
const [a, b] = range; // 1, 2
for (const n of range) console.log(n);
// partially consuming an iterator, then handing it on
const it = range[Symbol.iterator]();
it.next(); // { value: 1, done: false }
for (const n of it) console.log(n); // 2, 3 - continues from where it stopped- A
for...ofloop callsreturn()on the iterator when it exits early — that is how cleanup runs in a generator. - Strings, arrays, Map, Set, NodeList and generators are all iterable; plain objects are not.
- Iterators are single-use. Spread consumes them completely, so a second spread finds nothing.
Generator functions
A function* returns a generator that pauses at each yield and resumes with local state intact. The body does not run at all until you ask for the first value, so a generator can express an unbounded or very large sequence without allocating it.
function* takeWhile(iterable, predicate) {
for (const item of iterable) {
if (!predicate(item)) return; // return() is also triggered here
yield item;
}
}
function* naturals() {
let n = 1;
while (true) yield n++; // infinite, but only computed on demand
}
const small = [...takeWhile(naturals(), n => n < 5)]; // [1, 2, 3, 4]
// a generator can receive values back in
function* running() {
let total = 0;
for (;;) {
const value = yield total; // sent value arrives here
total += value ?? 0;
}
}
const g = running();
g.next(); // { value: 0, done: false } - primes it
g.next(10); // { value: 10, done: false }
g.next(5); // { value: 15, done: false }
// async generators: streams of awaited values
async function* pages(url) {
let next = url;
while (next) {
const res = await fetch(next);
if (!res.ok) throw new Error('HTTP ' + res.status);
const body = await res.json();
yield body.items;
next = body.nextUrl;
}
}
for await (const batch of pages('/api/items')) {
process(batch); // one page in memory at a time
}next() call, not where the generator was created — so wrap the loop, not the call. Also remember that a generator you never start never runs: a for await over an unused async generator performs no work at all.Map, Set and WeakMap
const m = new Map([['a', 1]]);
m.set({ id: 7 }, 'object key'); // any key type, including objects
m.get('a'); // 1
m.has('a'); // true
m.size; // 2
for (const [key, value] of m) console.log(key, value);
const obj = Object.fromEntries(m); // only when keys are strings
const s = new Set([1, 2, 2, 3]); // deduplicated on construction
s.add(4); s.delete(1); s.size; // 3
[...s]; // [2, 3, 4] - insertion order
// set algebra
const a = new Set([1, 2, 3]), b = new Set([2, 3, 4]);
new Set([...a].filter(x => b.has(x))); // intersection
new Set([...a, ...b]); // union
new Set([...a].filter(x => !b.has(x))); // difference
a.isSubsetOf(b); // modern runtimes
a.intersection(b); // newer still - check support
// dedupe objects by one property
const byId = new Map(users.map(u => [u.id, u]));
const unique = [...byId.values()];
// non-iterable side table keyed by object, garbage-collected with it
const cache = new WeakMap();
function memo(obj, compute) {
if (!cache.has(obj)) cache.set(obj, compute(obj));
return cache.get(obj);
}| Need | Use | Reason |
|---|---|---|
| Fixed record shape, JSON output | Object | Serialises directly |
| Frequent add and delete | Map | Tuned for mutation and gives size |
| Keys that are objects | Map or WeakMap | Object keys are coerced to strings |
| Membership tests on many values | Set | Average O(1) lookup instead of O(n) on an array |
| Attach data to a DOM node or object | WeakMap | No memory leak when the key is collected |
| Unique values from an array | new Set(arr) | Idiomatic one-liner; order is preserved |
A Map is not JSON-serialisable, so convert with Object.fromEntries at the boundary and keep the Map inside your own code. A WeakMap cannot be iterated by design — if you need to list the entries, you need a real Map plus your own cleanup.
FAQ
How do I pick between a generator and an array method?
Why is my generator returning an empty result?
Array.from or a previous loop exhausted it. Recreate it by calling the generator function again rather than reusing the object.Related
Classes, prototypes and object-oriented patterns The event loop, timers and concurrency
Last refreshed 2026-09-18.