Generators and Iterators

advanced
Published

June 18, 2024

JavaScript’s iterators and generators provide elegant ways to handle sequences of data efficiently. They are particularly useful when dealing with large datasets or situations where you want to produce values on demand, rather than generating them all upfront. This post will look at both concepts with clear explanations and code examples.

Iterators: Stepping Through Data

An iterator is an object that provides a standardized way to traverse a sequence of values. It implements the iterator protocol, which consists of a single method: next(). The next() method returns an object with two properties:

  • value: The next value in the sequence.
  • done: A boolean indicating whether the iteration is complete. true signifies the end.

Let’s create a simple iterator for an array:

function makeIterator(array) {
  let nextIndex = 0;
  return {
    next: function() {
      if (nextIndex < array.length) {
        return {
          value: array[nextIndex++],
          done: false
        };
      } else {
        return {
          done: true
        };
      }
    }
  };
}

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

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()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { done: true }

This example demonstrates the manual creation of an iterator. Many built-in JavaScript objects (like arrays and maps) are already iterable. You can use a for...of loop to conveniently iterate over them:

for (const value of myArray) {
  console.log(value); // Outputs 1, 2, 3, 4, 5
}

Generators: Simplifying Iterator Creation

Generators simplify the process of creating iterators. They use the function* syntax and the yield keyword:

function* numberGenerator(n) {
  for (let i = 0; i < n; i++) {
    yield i;
  }
}

const gen = numberGenerator(5);

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

The yield keyword pauses the generator’s execution and returns a value. The next time next() is called, execution resumes from where it left off. This makes generators incredibly useful for generating sequences on demand, preventing unnecessary computation.

Practical Applications

Generators and iterators have various applications:

  • Infinite Sequences: Generators can easily create infinite sequences (e.g., generating prime numbers).
  • Lazy Evaluation: Values are generated only when needed, improving performance, especially with large datasets.
  • Asynchronous Operations: Generators can be used with async/await to handle asynchronous operations more elegantly.
  • Custom Iterables: You can create custom iterable objects that behave seamlessly with for...of loops.

Iterators and generators are powerful tools in JavaScript’s arsenal. They provide efficient and elegant ways to work with sequences of data, particularly when dealing with large datasets or scenarios requiring on-demand value generation. Mastering these concepts enhances your ability to write clean, efficient, and maintainable JavaScript code.