Iterators & Iterables
Master the mechanics of JavaScript traversal. Understand the underlying protocols that power `for...of`, destructuring, and the spread operator to build memory-efficient, custom data streams.
The Two-Part Protocol
JavaScript iteration is built on two distinct contracts. The **Iterable Protocol** allows an object to define its iteration behavior (via `[Symbol.iterator]`), while the **Iterator Protocol** defines how values are actually produced (via the `next()` method).
// Architectural Logic: The Iteration Protocols
// 1. Iterable Protocol: Defines Symbol.iterator
// 2. Iterator Protocol: Defines next() returning { value, done }
const customCollection = {
items: ['Node.js', 'V8', 'Libuv'],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
const done = index >= this.items.length;
return {
value: !done ? this.items[index++] : undefined,
done
};
}
};
}
};
// Consumption via protocols
for (const tech of customCollection) {
console.log(tech); // Node.js, V8, Libuv
}O(1) Memory Traversal
One of the biggest advantages of Iterators is **lazy evaluation**. Instead of creating a massive array in memory, an iterator can calculate the next value only when it is requested. This allows us to "loop" over millions of items with virtually zero memory overhead.
// Performance Pattern: Memory-Efficient Range
// Creating an array of 1M items is O(N) memory.
// This iterator is O(1) memory.
class SmartRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
return {
next: () => ({
value: current <= this.end ? current++ : undefined,
done: current > this.end + 1
})
};
}
}
const largeRange = new SmartRange(1, 1_000_000);
// No memory spike! We generate values on demand.Iterator Delegation
JavaScript allows one iterator to "delegate" to another using the `yield*` syntax (often used in Generators, which we'll cover next). This is a powerful way to compose complex data streams from multiple sources.
// Advanced Pattern: Iterator Delegation
function* sequence() {
yield* [1, 2]; // Delegate to Array iterator
yield* "AB"; // Delegate to String iterator
}
console.log([...sequence()]); // [1, 2, 'A', 'B']Technical Insight: The "Done" State
When `done: true` is returned, the `value` is typically discarded by language constructs like `for...of`. However, the iterator protocol allows a final value to be returned alongside `done: true`. Most modern JS features ignore this, but some advanced generator patterns utilize it.
Iterator Checklist:
- ✅ **Protocol:** Ensure `[Symbol.iterator]` returns an object with a `next()` method.
- ✅ **State:** Keep track of the iteration index in a closure or class property.
- ✅ **Lazy Loading:** Use iterators for large datasets to prevent memory exhaustion.
- ✅ **Compatibility:** Custom iterables automatically work with `[...]` and `destructuring`.
- ✅ **Termination:** Always ensure your iterator has a clear exit condition (`done: true`).