Iterator Pattern
Concept
Category: Behavioral Pattern
The Iterator Pattern lets you traverse elements of a collection without exposing the collection’s underlying internal representation (whether it’s an Array, a Linked List, a Tree, or a Graph).
If you have a binary tree, walking through it requires complex recursive DFS or BFS logic.
The Iterator pattern abstracts all of that complexity away. It provides a simple, standard interface with two methods: hasNext() and next(). The client just calls next() in a while loop, completely oblivious to the fact that they are traversing a massive complex graph.
Implementation
// 1. The Iterator Interface
interface Iterator<T> {
hasNext(): boolean;
next(): T;
}
// 2. The Collection Interface
interface IterableCollection<T> {
createIterator(): Iterator<T>;
}
// 3. A Concrete Collection (e.g., A custom Linked List)
class CustomList implements IterableCollection<string> {
private items: string[] = [];
public addItem(item: string) {
this.items.push(item);
}
public getItems(): string[] {
return this.items;
}
// This is the magic! It returns the Iterator object.
public createIterator(): Iterator<string> {
return new CustomListIterator(this);
}
}
// 4. The Concrete Iterator
class CustomListIterator implements Iterator<string> {
private collection: CustomList;
private position: number = 0;
constructor(collection: CustomList) {
this.collection = collection;
}
public hasNext(): boolean {
// Are we at the end of the collection?
return this.position < this.collection.getItems().length;
}
public next(): string {
if (this.hasNext()) {
const item = this.collection.getItems()[this.position];
this.position++; // Move the pointer forward!
return item;
}
throw new Error("Out of bounds!");
}
}
// Usage:
const list = new CustomList();
list.addItem("Alice");
list.addItem("Bob");
list.addItem("Charlie");
// The Client uses the Iterator to walk the list!
const iterator = list.createIterator();
while (iterator.hasNext()) {
console.log(iterator.next());
// Prints "Alice", then "Bob", then "Charlie"
}
The Power of Multiple Iterators
Why not just put the next() method directly inside the CustomList class?
Because if CustomList tracks its own position, you can only ever have ONE loop running at a time! If a second user tries to loop through the list, they will start at position = 3 and get nothing.
By extracting the position state into a separate Iterator object, you can instantiate 100 different Iterators. They will all walk through the exact same list independently, each maintaining their own private position pointer!
Furthermore, you can write different types of iterators for the same collection!
For a Binary Tree, you could write a PreOrderIterator, an InOrderIterator, and a PostOrderIterator. The client just requests the one they want.
Native Iterators in JavaScript
You don’t actually need to write these classes in modern JS/TS. The Iterator pattern is built directly into the language via Generator Functions (function* and yield) and the Symbol.iterator property.
Any object that implements [Symbol.iterator]() can be instantly traversed using a standard for...of loop!
const myCustomCollection = {
items: ["Apple", "Banana", "Cherry"],
// Implement the native JS Iterator protocol!
*[Symbol.iterator]() {
for (let item of this.items) {
yield item; // Pauses execution and returns the item!
}
}
};
// The JS engine automatically calls next() under the hood!
for (const fruit of myCustomCollection) {
console.log(fruit);
}
Interview Questions
Q: In the JavaScript Generator yield example, why is it considered an Iterator?
A: yield is pure magic. When a function hits yield, it literally pauses its own execution, saves its entire internal state (all variables and line numbers) into RAM, and returns a value to the caller. The next time the caller asks for .next(), the function wakes up from hibernation, resumes from the exact line after the yield, and continues looping until it hits the next yield. It completely perfectly encapsulates the position state of an Iterator without needing a class.