Advanced JavaScript

JavaScript Iterators and Iterables: Custom Iteration Protocols

Master JavaScript's iteration protocols. Learn to create custom iterators and iterables, understand Symbol.iterator, and work with built-in iterables.

By JavaScript Document Teamโ€ข
iteratorsiterablesprotocolsadvancedes6

JavaScript's iteration protocols provide a standardized way to make objects iterable. Understanding these protocols allows you to create custom data structures that work seamlessly with for...of loops, spread syntax, and other iteration features.

Understanding Iteration Protocols

JavaScript defines two protocols for iteration:

  1. The Iterable Protocol - Defines how objects can be iterated
  2. The Iterator Protocol - Defines how to produce a sequence of values

The Iterator Protocol

An object is an iterator when it implements a next() method that returns an object with:

  • value: The next value in the sequence
  • done: Boolean indicating if iteration is complete
// Manual iterator
const simpleIterator = {
  current: 1,
  last: 5,

  next() {
    if (this.current <= this.last) {
      return { value: this.current++, done: false };
    }
    return { done: true };
  },
};

// Using the iterator manually
console.log(simpleIterator.next()); // { value: 1, done: false }
console.log(simpleIterator.next()); // { value: 2, done: false }
console.log(simpleIterator.next()); // { value: 3, done: false }
console.log(simpleIterator.next()); // { value: 4, done: false }
console.log(simpleIterator.next()); // { value: 5, done: false }
console.log(simpleIterator.next()); // { done: true }

The Iterable Protocol

An object is iterable when it implements the Symbol.iterator method that returns an iterator.

// Making an object iterable
const iterableObject = {
  data: ['a', 'b', 'c'],

  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;

    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        }
        return { done: true };
      },
    };
  },
};

// Now it can be used with for...of
for (const value of iterableObject) {
  console.log(value); // 'a', 'b', 'c'
}

// And with spread syntax
console.log([...iterableObject]); // ['a', 'b', 'c']

Built-in Iterables

Many JavaScript objects are iterable by default:

Arrays

const arr = [1, 2, 3];

// Getting the iterator
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { done: true }

// Arrays are iterable
for (const value of arr) {
  console.log(value); // 1, 2, 3
}

Strings

const str = 'Hello';

// Strings are iterable
for (const char of str) {
  console.log(char); // 'H', 'e', 'l', 'l', 'o'
}

// Spread into array
console.log([...str]); // ['H', 'e', 'l', 'l', 'o']

// Works with Unicode
const emoji = '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ';
console.log([...emoji]); // ['๐Ÿ‘จ', 'โ€', '๐Ÿ‘ฉ', 'โ€', '๐Ÿ‘ง', 'โ€', '๐Ÿ‘ฆ']

Maps and Sets

const map = new Map([
  ['a', 1],
  ['b', 2],
]);

// Maps are iterable (yields [key, value] pairs)
for (const [key, value] of map) {
  console.log(`${key}: ${value}`); // 'a: 1', 'b: 2'
}

const set = new Set([1, 2, 3]);

// Sets are iterable
for (const value of set) {
  console.log(value); // 1, 2, 3
}

Arguments Object

function testArguments() {
  // arguments is iterable
  for (const arg of arguments) {
    console.log(arg);
  }
}

testArguments('a', 'b', 'c'); // 'a', 'b', 'c'

Creating Custom Iterables

Range Iterator

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 ((step > 0 && current <= end) || (step < 0 && current >= end)) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { done: true };
      },
    };
  }
}

// Usage
const range = new Range(1, 10, 2);
console.log([...range]); // [1, 3, 5, 7, 9]

for (const num of new Range(5, 1, -1)) {
  console.log(num); // 5, 4, 3, 2, 1
}

Fibonacci Iterator

class Fibonacci {
  constructor(max = Infinity) {
    this.max = max;
  }

  [Symbol.iterator]() {
    let prev = 0;
    let curr = 1;
    let count = 0;
    const max = this.max;

    return {
      next() {
        if (count++ < max) {
          const value = curr;
          [prev, curr] = [curr, prev + curr];
          return { value, done: false };
        }
        return { done: true };
      },
    };
  }
}

// Get first 10 Fibonacci numbers
const fib10 = new Fibonacci(10);
console.log([...fib10]); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

// Use with for...of
for (const num of new Fibonacci(5)) {
  console.log(num); // 1, 1, 2, 3, 5
}

Tree Iterator

class TreeNode {
  constructor(value, left = null, right = null) {
    this.value = value;
    this.left = left;
    this.right = right;
  }
}

class BinaryTree {
  constructor(root) {
    this.root = root;
  }

  // In-order traversal
  *[Symbol.iterator]() {
    yield* this.inOrder(this.root);
  }

  *inOrder(node) {
    if (node) {
      yield* this.inOrder(node.left);
      yield node.value;
      yield* this.inOrder(node.right);
    }
  }

  // Pre-order traversal
  *preOrder(node = this.root) {
    if (node) {
      yield node.value;
      yield* this.preOrder(node.left);
      yield* this.preOrder(node.right);
    }
  }

  // Level-order traversal
  *levelOrder() {
    if (!this.root) return;

    const queue = [this.root];

    while (queue.length > 0) {
      const node = queue.shift();
      yield node.value;

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }
}

// Create a tree
const tree = new BinaryTree(
  new TreeNode(
    4,
    new TreeNode(2, new TreeNode(1), new TreeNode(3)),
    new TreeNode(6, new TreeNode(5), new TreeNode(7))
  )
);

// In-order traversal (default)
console.log([...tree]); // [1, 2, 3, 4, 5, 6, 7]

// Pre-order traversal
console.log([...tree.preOrder()]); // [4, 2, 1, 3, 6, 5, 7]

// Level-order traversal
console.log([...tree.levelOrder()]); // [4, 2, 6, 1, 3, 5, 7]

Advanced Iterator Patterns

Infinite Iterators

class InfiniteSequence {
  constructor(start = 0, step = 1) {
    this.start = start;
    this.step = step;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const step = this.step;

    return {
      next() {
        const value = current;
        current += step;
        return { value, done: false };
      },
    };
  }
}

// Take helper function
function* take(n, iterable) {
  let count = 0;
  for (const value of iterable) {
    if (count++ >= n) break;
    yield value;
  }
}

const infinite = new InfiniteSequence(10, 10);
console.log([...take(5, infinite)]); // [10, 20, 30, 40, 50]

Composable Iterators

// Filter iterator
function* filter(predicate, iterable) {
  for (const value of iterable) {
    if (predicate(value)) {
      yield value;
    }
  }
}

// Map iterator
function* map(fn, iterable) {
  for (const value of iterable) {
    yield fn(value);
  }
}

// Take while iterator
function* takeWhile(predicate, iterable) {
  for (const value of iterable) {
    if (!predicate(value)) break;
    yield value;
  }
}

// Composition example
const numbers = new InfiniteSequence(1);
const pipeline = takeWhile(
  (x) => x < 100,
  map(
    (x) => x * x,
    filter((x) => x % 2 === 0, numbers)
  )
);

console.log([...pipeline]); // [4, 16, 36, 64]

Stateful Iterators

class StatefulIterator {
  constructor(data) {
    this.data = data;
    this.index = 0;
  }

  [Symbol.iterator]() {
    return this; // Return itself as iterator
  }

  next() {
    if (this.index < this.data.length) {
      return { value: this.data[this.index++], done: false };
    }
    return { done: true };
  }

  reset() {
    this.index = 0;
  }

  skip(n) {
    this.index = Math.min(this.index + n, this.data.length);
  }
}

const stateful = new StatefulIterator(['a', 'b', 'c', 'd', 'e']);

console.log(stateful.next()); // { value: 'a', done: false }
console.log(stateful.next()); // { value: 'b', done: false }

stateful.skip(2);
console.log(stateful.next()); // { value: 'e', done: false }

stateful.reset();
console.log(stateful.next()); // { value: 'a', done: false }

Iterator Helpers

Zip Iterator

function* zip(...iterables) {
  const iterators = iterables.map((it) => it[Symbol.iterator]());

  while (true) {
    const results = iterators.map((it) => it.next());

    if (results.some((r) => r.done)) {
      break;
    }

    yield results.map((r) => r.value);
  }
}

const nums = [1, 2, 3];
const letters = ['a', 'b', 'c'];
const bools = [true, false, true];

for (const [num, letter, bool] of zip(nums, letters, bools)) {
  console.log(num, letter, bool);
}
// 1 'a' true
// 2 'b' false
// 3 'c' true

Chain Iterator

function* chain(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];

console.log([...chain(arr1, arr2, arr3)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Cycle Iterator

function* cycle(iterable) {
  const saved = [];

  for (const value of iterable) {
    yield value;
    saved.push(value);
  }

  while (saved.length > 0) {
    yield* saved;
  }
}

const colors = cycle(['red', 'green', 'blue']);
const tenColors = [...take(10, colors)];
console.log(tenColors);
// ['red', 'green', 'blue', 'red', 'green', 'blue', 'red', 'green', 'blue', 'red']

Async Iteration

Async Iterators

class AsyncRange {
  constructor(start, end, delay = 100) {
    this.start = start;
    this.end = end;
    this.delay = delay;
  }

  async *[Symbol.asyncIterator]() {
    for (let i = this.start; i <= this.end; i++) {
      await new Promise((resolve) => setTimeout(resolve, this.delay));
      yield i;
    }
  }
}

// Usage
(async () => {
  const asyncRange = new AsyncRange(1, 5, 500);

  for await (const num of asyncRange) {
    console.log(num); // Logs 1, 2, 3, 4, 5 with 500ms delay
  }
})();

// Async data fetcher
class AsyncDataFetcher {
  constructor(urls) {
    this.urls = urls;
  }

  async *[Symbol.asyncIterator]() {
    for (const url of this.urls) {
      try {
        const response = await fetch(url);
        const data = await response.json();
        yield data;
      } catch (error) {
        yield { error: error.message, url };
      }
    }
  }
}

Performance Considerations

// Iterator vs Array methods
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);

// Array methods create intermediate arrays
console.time('Array methods');
const result1 = largeArray
  .filter((x) => x % 2 === 0)
  .map((x) => x * 2)
  .slice(0, 10);
console.timeEnd('Array methods');

// Iterator approach - lazy evaluation
console.time('Iterator');
function* pipeline(arr) {
  let count = 0;
  for (const x of arr) {
    if (x % 2 === 0) {
      yield x * 2;
      if (++count >= 10) break;
    }
  }
}
const result2 = [...pipeline(largeArray)];
console.timeEnd('Iterator');

// Iterator is typically faster for early termination

Common Pitfalls

Iterator Reuse

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();

console.log([...iterator]); // [1, 2, 3]
console.log([...iterator]); // [] - iterator is exhausted!

// Solution: Create new iterator
const iter1 = arr[Symbol.iterator]();
const iter2 = arr[Symbol.iterator]();
console.log([...iter1]); // [1, 2, 3]
console.log([...iter2]); // [1, 2, 3]

Iterator vs Iterable

// This is an iterator (has next method)
const iterator = {
  current: 0,
  next() {
    return this.current < 3
      ? { value: this.current++, done: false }
      : { done: true };
  },
};

// Cannot use with for...of
// for (const value of iterator) {} // Error!

// Make it iterable
iterator[Symbol.iterator] = function () {
  return this;
};

// Now it works
for (const value of iterator) {
  console.log(value); // 0, 1, 2
}

Best Practices

  1. Make custom objects iterable when they represent collections
  2. Use generators for simpler iterator implementation
  3. Implement async iterators for asynchronous data sources
  4. Consider memory efficiency with lazy evaluation
  5. Document iteration behavior clearly
  6. Handle edge cases like empty collections

Conclusion

Iterators and iterables are fundamental to modern JavaScript, providing a standardized way to work with sequences of values. Understanding these protocols enables you to create custom data structures that integrate seamlessly with JavaScript's iteration features, leading to more expressive and efficient code.