Generators and Iterators in JavaScript

In JavaScript, iterators and generators are powerful tools that allow you to work with data in a more efficient and controlled way, especially when dealing with large datasets or complex iterations.

Generators

  • A JavaScript generator function is a special function that can pause and resume execution.
  • They are created using the function* syntax and can be paused and resumed at any point during their execution using the yield keyword.
  • When a generator function is called, it returns a generator object, an iterator.

Use cases:

  • Generators can handle large datasets without loading the entire set into memory.
  • They allow more complex iteration patterns than traditional loops or iterable constructs.
  • With the advent of async/await, built on generators, they can simplify asynchronous code by making it look more like synchronous code.

Basic Example

// Define a generator function
function* generatorExample() {
    yield 1; // Pauses and returns 1
    yield 2; // Pauses and returns 2
    yield 3; // Pauses and returns 3
}

// Create a generator object
const gen = generatorExample();

console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next().value); // undefined

Infinite Generator Example

You can create a generator that never stops producing values:

function* infiniteGenerator() {
    let index = 0;
    while (true) {
        yield index++;
    }
}

const infGen = infiniteGenerator();

console.log(infGen.next().value); // 0
console.log(infGen.next().value); // 1
console.log(infGen.next().value); // 2
console.log(infGen.next().value); // 3

Generator with async/await

async function* asyncGenerator() {
    const urls = [
	    'https://api.example.com/data1', 
	    'https://api.example.com/data2', 
	    'https://api.example.com/data3'
	];
    
    for (const url of urls) {
        const response = await fetch(url); // Await the async fetch call
        const data = await response.json(); // Await the conversion to JSON
        yield data; // Yield the data one at a time
    }
}

(async () => {
    const gen = asyncGenerator();
    for await (const data of gen) {
        console.log(data); // Logs each data object as it's received
    }
})();

Iterators and Iterables

An iterator is an object that provides a way to access elements in a collection one at a time.

Think of it like a pointer that moves through the collection, giving you the current element each time you request it.

Built-in iterators exist for arrays, strings, maps, sets, and other iterable objects.

You can use the for...of loop to easily iterate over iterables.

Here is an analogy: Imagine iterating over a playlist. An iterator would be like a song player that goes to the next song each time you press a button.

Example

const numbers = [1, 2, 3, 4, 5];

const numberIterator = numbers[Symbol.iterator]();

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

Create a Custom Iterator

  • To create a custom iterator, you need to define an object with a next() method that return an object object with two properties: value and done.
  • The value property holds the current item in the collection, and the done property is a boolean indicating whether the iteration has reached the end of the collection.
function createCustomIterator(array) {
  let index = 0;

  return {
    next: function() {
      if (index < array.length) {
        const value = array[index] + 1; // Increment each number by 1
        index++;
        return { value, done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

// Use the custom iterator
const myArray = [1, 2, 3, 4, 5];
const iterator = createCustomIterator(myArray);

let result = iterator.next();
while (!result.done) {
  console.log(result.value);
  result = iterator.next();
}

Create Iterable Protocol

  • To make an object iterable using the for...of loop, you need to implement the iterable protocol by defining a [Symbol.iterator] method.
const customIterable = {
  array: [1, 2, 3, 4, 5],
  [Symbol.iterator]: function() {
    let index = 0;
    let array = this.array;

    return {
      next: function() {
        if (index < array.length) {
          const value = array[index] + 1;
          index++;
          return { value, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

// Use the custom iterable with a for...of loop
for (let value of customIterable) {
  console.log(value);
}

Summary

  • In JavaScript, both generators and iterators handle sequences of values, but they have different purposes and functionalities.
  • Generator is a function that produces or yields a sequence of values using the yield statement. You can think of it as a function that can be paused and resumed.
  • Iterator is an object that provides a mechanism for traversing a collection of data, one element at a time.
  • Iterable is an object that has [Symbol.iterator] method. This method must return an iterator object.