Iterators & Generators
Iterators & Generators
JavaScript's iteration protocol is the mechanism behind for...of, spread syntax, and destructuring. Generators extend that protocol with the ability to produce values lazily, one at a time.
The Iteration Protocol
An object is iterable if it has a [Symbol.iterator] method that returns an iterator — an object with a next() method that returns { value, done }.
const iterable = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
return i < 3 ? { value: i++, done: false }
: { value: undefined, done: true };
}
};
}
};
[...iterable]; // [0, 1, 2]for...of
for...of works with any iterable: arrays, strings, Sets, Maps, generators, and your own custom iterables.
for (const char of "hello") console.log(char);Do not confuse for...of (values from an iterable) with for...in (enumerable keys of an object).
Generator Functions
A generator function is declared with function* and can yield multiple values over time:
function* count(from, to) {
for (let i = from; i <= to; i++) {
yield i;
}
}
const gen = count(1, 3);
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }Why Generators?
- Lazy evaluation — values are computed only when requested
- Infinite sequences — generate numbers, IDs, or events without holding all in memory
- Pausable execution — control flow can be suspended and resumed
yield*
yield* delegates to another iterable, useful for composing generators:
function* concat(...iterables) {
for (const it of iterables) yield* it;
}Code Examples
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) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const range = new Range(1, 10, 2);
// Works with for...of
for (const n of range) process.stdout.write(n + " ");
console.log();
// Works with spread
console.log([...new Range(0, 6, 3)]);
// Works with destructuring
const [first, second, third] = new Range(10, 50, 10);
console.log(first, second, third);Implementing Symbol.iterator makes any object work with for...of, spread, and destructuring transparently. The iterator maintains its own state (current) independently of the iterable.
// Infinite sequence generator (lazy!)
function* naturals(start = 1) {
let n = start;
while (true) yield n++;
}
// Take first N values from any iterable
function take(iterable, n) {
const result = [];
for (const val of iterable) {
result.push(val);
if (result.length >= n) break;
}
return result;
}
console.log("First 5 naturals:", take(naturals(), 5));
console.log("Naturals from 10:", take(naturals(10), 5));
// Fibonacci generator
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
console.log("First 8 Fibonacci:", take(fibonacci(), 8));
// Generator with return value
function* countdown(from) {
while (from > 0) yield from--;
return "liftoff!";
}
const cd = countdown(3);
let result;
while (!(result = cd.next()).done) {
console.log(result.value);
}
console.log(result.value); // the return valueGenerators are lazy — they compute values only when next() is called. An infinite generator is safe because we control when to stop. The return value appears in the final { value, done: true } object.
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
// yield* delegates to another iterable
function* concat(...iterables) {
for (const it of iterables) yield* it;
}
console.log([...concat(range(1, 3), range(7, 9), [20, 21])]);
// Generator pipeline: map + filter lazily
function* map(iter, fn) {
for (const val of iter) yield fn(val);
}
function* filter(iter, pred) {
for (const val of iter) if (pred(val)) yield val;
}
// Squares of even numbers from 1–20, lazily computed
const pipeline = filter(
map(range(1, 20), (n) => n * n),
(n) => n % 2 === 0
);
console.log([...pipeline]);yield* delegates iteration to another iterable in-line. Generator pipelines compose map/filter lazily — each value flows through the entire pipeline before the next one is computed, with no intermediate arrays allocated.
Quick Quiz
1. What method must an object implement to be considered iterable?
2. What does a generator function return when called?
3. What is the key advantage of lazy generators over arrays for large sequences?
Was this lesson helpful?