Iterator Pattern

designpatterns
Published

June 22, 2024

The Iterator pattern is a fundamental behavioral design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. In simpler terms, it lets you traverse through a collection (like an array, list, or even a custom object) without needing to know the specifics of how that collection is stored internally. This promotes loose coupling and makes your code more flexible and maintainable.

Why Use the Iterator Pattern?

  • Abstraction: Hides the complexity of traversing different data structures. You use the same interface to iterate regardless of whether you’re dealing with an array, a linked list, or a tree.
  • Flexibility: Easily add new types of collections or change existing ones without affecting the code that uses them.
  • Maintainability: Changes to the internal structure of a collection don’t require modifications to the code iterating over it.
  • Simplified Code: Iterators often encapsulate complex traversal logic, leading to cleaner and more readable code.

Implementing the Iterator Pattern in JavaScript

JavaScript doesn’t have a built-in Iterator interface like some other languages (e.g., Java). However, we can easily implement it using a combination of objects and functions.

Example 1: Iterating over an Array

Let’s create a simple iterator for an array:

class ArrayIterator {
  constructor(array) {
    this.array = array;
    this.index = 0;
  }

  hasNext() {
    return this.index < this.array.length;
  }

  next() {
    if (this.hasNext()) {
      return this.array[this.index++];
    }
    return null; // Or throw an exception
  }
}

const myArray = [1, 2, 3, 4, 5];
const iterator = new ArrayIterator(myArray);

while (iterator.hasNext()) {
  console.log(iterator.next());
}

This example demonstrates a basic iterator. hasNext() checks if there are more elements, and next() returns the next element.

Example 2: Iterating over a Custom Object

Now let’s create an iterator for a more complex data structure:

class BookCollection {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  getIterator() {
    return new BookCollectionIterator(this.books);
  }
}

class BookCollectionIterator {
  constructor(books) {
    this.books = books;
    this.index = 0;
  }

  hasNext() {
    return this.index < this.books.length;
  }

  next() {
    if (this.hasNext()) {
      return this.books[this.index++];
    }
    return null;
  }
}


const collection = new BookCollection();
collection.addBook({ title: "The Lord of the Rings", author: "J.R.R. Tolkien" });
collection.addBook({ title: "Pride and Prejudice", author: "Jane Austen" });

const iterator2 = collection.getIterator();

while (iterator2.hasNext()) {
  console.log(iterator2.next());
}

This example shows how to create an iterator specifically for a BookCollection. The getIterator() method returns a new iterator instance, encapsulating the traversal logic.

Example 3: Using JavaScript’s built-in iterators (for…of loop)

Many built-in JavaScript objects already support iteration using the for...of loop. This uses JavaScript’s built-in iterator functionality:

const myArray2 = [6,7,8,9,10];
for (const element of myArray2) {
  console.log(element);
}

//Iterating over a Map
const myMap = new Map([['a',1],['b',2]]);
for (const [key, value] of myMap) {
  console.log(key, value);
}

//Iterating over a Set
const mySet = new Set([1,2,3,4,5]);
for (const element of mySet) {
    console.log(element);
}

This illustrates how built-in iterators simplify code considerably when available.

These examples highlight the flexibility and power of the Iterator pattern. By abstracting away the details of traversal, you create more maintainable code. Adapting this pattern for your specific needs enables cleaner, more organized code, especially when dealing with complex data structures.