Skip to main content
โšก Calmops

Generators and Iterators in JavaScript

Generators and Iterators in JavaScript

Generators and iterators are powerful features for creating lazy, efficient code. This article covers understanding iteration protocols, creating generators, and practical applications.

Introduction

Generators and iterators enable:

  • Lazy evaluation of sequences
  • Memory-efficient processing
  • Simplified state management
  • Elegant async patterns
  • Custom iteration logic

Understanding generators helps you:

  • Write more efficient code
  • Handle large datasets
  • Implement complex workflows
  • Build powerful abstractions

Iteration Protocol

Iterable Protocol

// An object is iterable if it has a Symbol.iterator method
// that returns an iterator

const iterable = {
  [Symbol.iterator]() {
    return {
      next() {
        // Return { value, done }
      }
    };
  }
};

// Can be used in for...of loops
for (const value of iterable) {
  console.log(value);
}

Iterator Protocol

// An iterator has a next() method that returns:
// { value: any, done: boolean }

const iterator = {
  next() {
    return { value: 1, done: false };
  }
};

// Call next() to get values
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 1, done: false }

Creating Custom Iterables

// โœ… Good: Create a custom iterable
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

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

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
}

// Usage
const range = new Range(1, 5);
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}
// Practical example: Iterable for object entries
class ObjectEntries {
  constructor(obj) {
    this.obj = obj;
  }

  [Symbol.iterator]() {
    const keys = Object.keys(this.obj);
    let index = 0;

    return {
      next: () => {
        if (index < keys.length) {
          const key = keys[index++];
          return {
            value: [key, this.obj[key]],
            done: false
          };
        }
        return { done: true };
      }
    };
  }
}

// Usage
const obj = { a: 1, b: 2, c: 3 };
const entries = new ObjectEntries(obj);

for (const [key, value] of entries) {
  console.log(`${key}: ${value}`);
}

Generator Functions

Basic Generators

// Generator function: uses function* and yield
function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

// Calling generator returns an iterator
const gen = simpleGenerator();

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: undefined, done: true }
// Generators are iterable
function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}

// Can be used in for...of
for (const num of countTo(5)) {
  console.log(num); // 1, 2, 3, 4, 5
}

// Can be spread
const numbers = [...countTo(5)];
console.log(numbers); // [1, 2, 3, 4, 5]

Generator State

// Generators maintain state between yields
function* counter() {
  let count = 0;
  while (true) {
    yield count++;
  }
}

const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

Sending Values to Generators

// Send values back to generator with next(value)
function* echo() {
  const value1 = yield 'first';
  console.log('Received:', value1);
  
  const value2 = yield 'second';
  console.log('Received:', value2);
}

const gen = echo();

console.log(gen.next());        // { value: 'first', done: false }
console.log(gen.next('hello')); // Logs: Received: hello
                                // { value: 'second', done: false }
console.log(gen.next('world')); // Logs: Received: world
                                // { value: undefined, done: true }
// Practical example: Generator with input
function* fibonacci(limit) {
  let a = 0, b = 1;
  
  while (a < limit) {
    const next = yield a;
    
    // If value sent, use it; otherwise continue sequence
    if (next !== undefined) {
      a = next;
    } else {
      [a, b] = [b, a + b];
    }
  }
}

const gen = fibonacci(100);
console.log(gen.next().value);      // 0
console.log(gen.next().value);      // 1
console.log(gen.next().value);      // 1
console.log(gen.next(50).value);    // 50 (sent value)
console.log(gen.next().value);      // 51

Practical Generator Patterns

Lazy Sequences

// โœ… Good: Lazy evaluation with generators
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

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

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

// Lazy evaluation: only computes what's needed
const result = filter(
  map(range(1, 1000000), x => x * 2),
  x => x % 3 === 0
);

// Only processes first 5 values
for (const value of result) {
  console.log(value);
  if (value > 30) break;
}

Reading Files Line by Line

// โœ… Good: Read large files efficiently
async function* readLines(filePath) {
  const fs = require('fs');
  const readline = require('readline');
  
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

// Usage: Process large file without loading into memory
async function processFile(filePath) {
  for await (const line of readLines(filePath)) {
    console.log(line);
  }
}

Infinite Sequences

// โœ… Good: Generate infinite sequences
function* infiniteSequence(start = 0, step = 1) {
  let current = start;
  while (true) {
    yield current;
    current += step;
  }
}

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

// Usage
const evens = take(infiniteSequence(0, 2), 5);
console.log([...evens]); // [0, 2, 4, 6, 8]

Delegating to Other Generators

// โœ… Good: Use yield* to delegate
function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield 3;
  yield 4;
}

function* combined() {
  yield* generator1();
  yield* generator2();
}

console.log([...combined()]); // [1, 2, 3, 4]
// Practical example: Tree traversal
function* traverse(node) {
  yield node.value;
  
  if (node.left) {
    yield* traverse(node.left);
  }
  
  if (node.right) {
    yield* traverse(node.right);
  }
}

// Usage
const tree = {
  value: 1,
  left: { value: 2, left: null, right: null },
  right: { value: 3, left: null, right: null }
};

for (const value of traverse(tree)) {
  console.log(value); // 1, 2, 3
}

Generator Methods

return() Method

// return() terminates the generator
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();
console.log(g.next());    // { value: 1, done: false }
console.log(g.return(99)); // { value: 99, done: true }
console.log(g.next());    // { value: undefined, done: true }

throw() Method

// throw() injects an error into the generator
function* gen() {
  try {
    yield 1;
    yield 2;
  } catch (error) {
    console.log('Caught:', error.message);
    yield 3;
  }
}

const g = gen();
console.log(g.next());           // { value: 1, done: false }
console.log(g.throw(new Error('oops'))); // Logs: Caught: oops
                                         // { value: 3, done: false }

Advanced Patterns

Generator-Based Coroutines

// โœ… Good: Use generators for async-like control flow
function* fetchData() {
  try {
    const response = yield fetch('/api/data');
    const data = yield response.json();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

// Runner function to execute generator
function run(generator) {
  const iterator = generator();
  
  function handle(result) {
    if (result.done) return;
    
    Promise.resolve(result.value)
      .then(res => handle(iterator.next(res)))
      .catch(err => iterator.throw(err));
  }
  
  handle(iterator.next());
}

// Usage
run(fetchData);

State Machine with Generators

// โœ… Good: Implement state machine
function* trafficLight() {
  while (true) {
    yield 'red';
    yield 'yellow';
    yield 'green';
  }
}

const light = trafficLight();
console.log(light.next().value);  // red
console.log(light.next().value);  // yellow
console.log(light.next().value);  // green
console.log(light.next().value);  // red

Memoization with Generators

// โœ… Good: Cache results with generators
function* memoize(fn) {
  const cache = new Map();
  
  while (true) {
    const arg = yield;
    
    if (cache.has(arg)) {
      console.log('From cache:', arg);
    } else {
      const result = fn(arg);
      cache.set(arg, result);
      console.log('Computed:', arg);
    }
  }
}

const memo = memoize(x => x * 2);
memo.next();           // Start generator
memo.next(5);          // Logs: Computed: 5
memo.next(5);          // Logs: From cache: 5

Best Practices

  1. Use generators for lazy evaluation:

    // โœ… Good
    function* lazyRange(n) {
      for (let i = 0; i < n; i++) {
        yield i;
      }
    }
    
  2. Delegate with yield:*

    // โœ… Good
    function* combined() {
      yield* generator1();
      yield* generator2();
    }
    
  3. Handle errors properly:

    // โœ… Good
    function* gen() {
      try {
        yield value;
      } catch (error) {
        // Handle error
      }
    }
    
  4. Use for large datasets:

    // โœ… Good - memory efficient
    function* readLargeFile(path) {
      // Yield one item at a time
    }
    

Common Mistakes

  1. Forgetting function syntax:*

    // โŒ Bad
    function gen() {
      yield 1;
    }
    
    // โœ… Good
    function* gen() {
      yield 1;
    }
    
  2. Not understanding lazy evaluation:

    // โŒ Bad - evaluates all values
    const values = [1, 2, 3, 4, 5].map(x => x * 2);
    
    // โœ… Good - lazy evaluation
    function* values() {
      for (let i = 1; i <= 5; i++) {
        yield i * 2;
      }
    }
    
  3. Forgetting generators are iterators:

    // โŒ Bad
    const gen = generator();
    gen.next();
    gen.next();
    
    // โœ… Good - use for...of
    for (const value of generator()) {
      console.log(value);
    }
    

Summary

Generators and iterators are powerful tools. Key takeaways:

  • Iterables have Symbol.iterator method
  • Iterators have next() method
  • Generators use function* and yield
  • Generators are lazy and memory-efficient
  • Use yield* to delegate to other generators
  • Generators can receive values via next(value)
  • Perfect for large datasets and sequences

Next Steps

Comments