Iterator Design Pattern
I built a custom data structure — a paginated result set from an API. It wasn't an array, but I wanted to loop over it with for...of. I didn't want to exp...
23 Mar 2024

I built a custom data structure — a paginated result set from an API. It wasn't an array, but I wanted to loop over it with for...of. I didn't want to expose the internal pagination logic to every consumer.
The Iterator pattern gives you a standard way to traverse a collection without revealing its internal structure. The consumer gets elements one at a time. It doesn't know if the data is in an array, a linked list, a database cursor, or fetched from a remote API.
Think of it like a TV remote with "next channel" and "previous channel" buttons. You don't know how the channels are stored internally. You just press the button and get the next one.
In JavaScript, the Iterator protocol is built into the language. Any object with a [Symbol.iterator]() method that returns { next() } works with for...of, spread syntax, and destructuring.
class List {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
[Symbol.iterator]() {
let index = 0;
const items = this.items;
return {
next() {
return index < items.length
? { value: items[index++], done: false }
: { done: true };
}
};
}
}
const list = new List();
list.add("Item 1");
list.add("Item 2");
list.add("Item 3");
// Works with for...of
for (const item of list) {
console.log(item);
}
// Works with spread
const array = [...list];
console.log(array); // ["Item 1", "Item 2", "Item 3"]
// Works with destructuring
const [first, second] = list;
console.log(first, second); // Item 1 Item 2
The List class hides its internal items array. Consumers traverse it through the iterator protocol. If I later change the internal storage to a linked list or a lazy generator, the consuming code doesn't change.
Generator Functions: The Shortcut
JavaScript generators make implementing iterators trivial:
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
for (const num of new Range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
The * generator syntax handles the { value, done } protocol automatically. Less boilerplate, same result.
The benefit: Uniform traversal interface. Consumer code works the same regardless of the underlying data structure. Lazy evaluation is possible — generate items on demand instead of loading everything into memory.
The cost: Iterators are forward-only by default. No random access, no going backward (without building that yourself). For simple arrays, using the iterator protocol directly adds complexity with no benefit — just use the array.
I use custom iterators for paginated API results, tree traversals, and lazy sequences. For standard arrays and sets, JavaScript's built-in iterators are already there — just use for...of.