Back to course

Iterators & Generators

Content Reader1 words • 0:00 • Browser TTS

image.png

Iterators & Generators – The Art of Controlled Iteration

Imagine you're a master storyteller 📖 sitting around a campfire with an eager audience. Unlike reading a book where pages are turned in sequence, your storytelling has special powers:

  1. Pause and Resume 🎭 - You can pause mid-sentence, let the audience discuss, then continue exactly where you left off
  2. Interactive Storytelling 🗣️ - The audience can influence the story direction, and you adapt accordingly
  3. Infinite Tales ♾️ - You can tell stories that theoretically never end, generating new chapters on demand
  4. Memory Efficiency 💭 - You don't need to remember the entire story at once - just the current scene and how to get to the next one
  5. Custom Pacing ⏱️ - You control exactly when each part of the story is revealed

Iterators and generators work exactly like this master storyteller. They give you complete control over how data is accessed and produced, allowing you to create:

  • Custom iteration patterns that go beyond simple array-like access
  • Lazy evaluation that produces values only when needed
  • Infinite sequences that generate data on-demand
  • Pausable functions that can yield control and resume later
  • Memory-efficient data processing for large datasets

This isn't just syntactic sugar - iterators and generators represent a fundamental shift toward more sophisticated control flow patterns that enable elegant solutions to complex problems.

The Theoretical Foundation: Lazy Evaluation and Control Flow 📐

Understanding Lazy Evaluation Theory

Iterators and generators implement "lazy evaluation" - a fundamental concept in computer science where computations are deferred until their results are actually needed.

This contrasts with eager evaluation (the default in most languages) where expressions are evaluated immediately when encountered.

Why Lazy Evaluation Matters:

  1. Memory Efficiency: Only compute and store what you need, when you need it
  2. Performance: Avoid expensive computations that might never be used
  3. Infinite Sequences: Represent infinite data structures in finite memory
  4. Composability: Chain operations without creating intermediate collections

Real-World Analogy: Think of lazy evaluation like a smart restaurant. Instead of preparing every dish on the menu at opening time (eager), they prepare dishes only when customers order them (lazy). This saves ingredients, reduces waste, and ensures freshness.

The Computer Science of Iterators

Iterators implement the "Iterator Pattern" from design patterns, providing a uniform interface for traversing collections without exposing their internal structure.

Core Concepts:

  • Separation of Concerns: Iteration logic separate from data structure
  • Uniform Interface: Same protocol works for arrays, trees, graphs, or custom structures
  • Stateful Traversal: Maintains position without exposing implementation details
  • Protocol-Based Design: Duck typing - anything with next() method is iterable

Generator Functions: Cooperative Multitasking

Generators implement "cooperative multitasking" - a concurrency model where functions voluntarily yield control rather than being preemptively interrupted.

Theoretical Background:

  1. Coroutines: Functions that can pause and resume execution at specific points
  2. State Machines: Generators maintain state between yields, like finite state machines
  3. Continuation Passing: Each yield point is effectively a continuation
  4. Cooperative Scheduling: Function controls when to give up execution time

This enables:

  • Pausable Functions: Break long-running operations into chunks
  • Two-Way Communication: Send values both in and out of functions
  • Custom Control Flow: Build your own iteration patterns
  • Memory-Efficient Processing: Stream processing without buffering entire datasets

The Philosophy of Pull vs Push

Traditional iteration is "pull-based" - the consumer pulls values from the producer:

Consumer: "Give me the next value"
Producer: "Here it is" or "I'm done"

Generators enable "push-like" patterns where the producer can control the flow:

Producer: "Here's a value, process it"
Consumer: "OK, give me the next one when ready"

This inverts control and enables more sophisticated data processing patterns like reactive programming and stream processing.

Understanding the Iterator Protocol 🔄

The Problem with Traditional Iteration 😤

Before iterators, JavaScript had limited ways to make objects iterable:

// Traditional array iteration
const numbers = [1, 2, 3, 4, 5];
for (let i = 0; i < numbers.length; i++) {
  console.log(numbers[i]);
}

// Object iteration required Object.keys()
const user = { name: "Alice", age: 30, city: "New York" };
for (let key of Object.keys(user)) {
  console.log(key, user[key]);
}

// Custom objects couldn't be iterated naturally
const customCollection = {
  data: [1, 2, 3, 4, 5],
  // No built-in way to make this object work with for...of
};

// This doesn't work:
// for (let value of customCollection) { ... } // TypeError!

Problems with traditional approaches:

  • Limited to built-in types: Only arrays and strings were naturally iterable
  • No customization: Couldn't control how iteration worked
  • Memory inefficient: All data had to exist in memory at once
  • No lazy evaluation: Values couldn't be generated on-demand

The Iterator Protocol Solution ✨

The iterator protocol provides a standard way to define how objects are iterated:

// Iterator protocol: objects must have a Symbol.iterator method
// that returns an iterator (object with next() method)

const customIterable = {
  data: [1, 2, 3, 4, 5],
  
  // Symbol.iterator method makes the object iterable
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    
    // Return an iterator object
    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// Now it works with for...of!
for (let value of customIterable) {
  console.log(value); // 1, 2, 3, 4, 5
}

// Also works with spread operator
const array = [...customIterable]; // [1, 2, 3, 4, 5]

// And with destructuring
const [first, second, ...rest] = customIterable;
console.log(first, second, rest); // 1, 2, [3, 4, 5]

How the Iterator Protocol Works 🔧

Key Components:

  1. Iterable: Object with a Symbol.iterator method
  2. Iterator: Object with a next() method
  3. Result: Object with value and done properties
// Understanding the protocol step by step
function createNumberIterator(start, end) {
  let current = start;
  
  // Return an iterator object
  return {
    next() {
      if (current <= end) {
        // Return { value: someValue, done: false }
        return { value: current++, done: false };
      } else {
        // Return { done: true } when iteration is complete
        return { done: true };
      }
    }
  };
}

// Manual iteration using the iterator
const iterator = createNumberIterator(1, 3);

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 }

// Creating an iterable object that uses our iterator
const numberRange = {
  start: 1,
  end: 5,
  
  [Symbol.iterator]() {
    return createNumberIterator(this.start, this.end);
  }
};

console.log([...numberRange]); // [1, 2, 3, 4, 5]

Advanced Iterator Examples 🚀

Example 1: Fibonacci Sequence Iterator

function createFibonacciIterator(maxCount = Infinity) {
  let prev = 0;
  let current = 1;
  let count = 0;
  
  return {
    next() {
      if (count >= maxCount) {
        return { done: true };
      }
      
      if (count === 0) {
        count++;
        return { value: prev, done: false };
      }
      
      if (count === 1) {
        count++;
        return { value: current, done: false };
      }
      
      const next = prev + current;
      prev = current;
      current = next;
      count++;
      
      return { value: prev, done: false };
    }
  };
}

// Create an iterable Fibonacci sequence
const fibonacci = {
  maxCount: 10,
  
  [Symbol.iterator]() {
    return createFibonacciIterator(this.maxCount);
  }
};

console.log([...fibonacci]); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Or create infinite sequence (careful with this!)
const infiniteFibonacci = {
  [Symbol.iterator]() {
    return createFibonacciIterator(); // No limit
  }
};

// Take first 15 Fibonacci numbers
const first15 = [];
for (let num of infiniteFibonacci) {
  if (first15.length >= 15) break;
  first15.push(num);
}
console.log(first15); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]

Example 2: Tree Node Iterator

class TreeNode {
  constructor(value, children = []) {
    this.value = value;
    this.children = children;
  }
  
  // Depth-first traversal iterator
  *depthFirst() {
    yield this.value;
    for (let child of this.children) {
      yield* child.depthFirst();
    }
  }
  
  // Breadth-first traversal iterator
  *breadthFirst() {
    const queue = [this];
    
    while (queue.length > 0) {
      const node = queue.shift();
      yield node.value;
      queue.push(...node.children);
    }
  }
  
  // Make the tree iterable (default to depth-first)
  [Symbol.iterator]() {
    return this.depthFirst();
  }
}

// Create a tree structure
const tree = new TreeNode("root", [
  new TreeNode("child1", [
    new TreeNode("grandchild1"),
    new TreeNode("grandchild2")
  ]),
  new TreeNode("child2", [
    new TreeNode("grandchild3")
  ])
]);

// Iterate using default (depth-first)
console.log("Depth-first:", [...tree]);
// ["root", "child1", "grandchild1", "grandchild2", "child2", "grandchild3"]

// Iterate using breadth-first
console.log("Breadth-first:", [...tree.breadthFirst()]);
// ["root", "child1", "child2", "grandchild1", "grandchild2", "grandchild3"]

Generator Functions – Simplified Iterator Creation 🎭

Understanding Generator Functions 💡

What are generators? Special functions that can pause and resume their execution, automatically implementing the iterator protocol.

Mental Model: Think of generators like a remote control for function execution - you can play, pause, rewind, and even send information back to the function while it's paused.

Basic Generator Syntax 📝

// Generator function syntax: function* (note the asterisk)
function* simpleGenerator() {
  console.log("Generator started");
  yield 1;
  console.log("After first yield");
  yield 2;
  console.log("After second yield");  
  yield 3;
  console.log("Generator ending");
  return "Done!";
}

// Calling a generator function returns a generator object
const gen = simpleGenerator();
console.log(gen); // [object Generator]

// Use next() to advance through the generator
console.log(gen.next()); // "Generator started" -> { value: 1, done: false }
console.log(gen.next()); // "After first yield" -> { value: 2, done: false }
console.log(gen.next()); // "After second yield" -> { value: 3, done: false }
console.log(gen.next()); // "Generator ending" -> { value: "Done!", done: true }
console.log(gen.next()); // { value: undefined, done: true }

// Generators are automatically iterable
function* numberGenerator() {
  yield 10;
  yield 20;
  yield 30;
}

// Works with for...of
for (let num of numberGenerator()) {
  console.log(num); // 10, 20, 30
}

// Works with spread operator  
const numbers = [...numberGenerator()]; // [10, 20, 30]

// Works with destructuring
const [first, second, third] = numberGenerator();
console.log(first, second, third); // 10, 20, 30

Generator Features and Capabilities 🔧

Feature 1: Yielding Values and Expressions

function* mathGenerator() {
  yield 1 + 1;                    // yield expressions
  yield Math.PI;                  // yield computed values
  yield new Date().getFullYear(); // yield function results
  
  const array = [1, 2, 3];
  yield* array;                   // yield* delegates to another iterable
  
  yield* "hello";                 // strings are iterable
}

console.log([...mathGenerator()]); 
// [2, 3.14159..., 2025, 1, 2, 3, "h", "e", "l", "l", "o"]

Feature 2: Two-Way Communication

function* interactiveGenerator() {
  const input1 = yield "What's your name?";
  console.log(`Hello, ${input1}!`);
  
  const input2 = yield "What's your age?";
  console.log(`You are ${input2} years old.`);
  
  const input3 = yield "What's your favorite color?";
  return `Nice to meet you, ${input1}! ${input3} is a great color.`;
}

const gen = interactiveGenerator();

console.log(gen.next());              // { value: "What's your name?", done: false }
console.log(gen.next("Alice"));       // "Hello, Alice!" -> { value: "What's your age?", done: false }
console.log(gen.next(30));            // "You are 30 years old." -> { value: "What's your favorite color?", done: false }
console.log(gen.next("Blue"));        // { value: "Nice to meet you, Alice! Blue is a great color.", done: true }

Feature 3: Error Handling in Generators

function* errorHandlingGenerator() {
  try {
    yield "Step 1";
    yield "Step 2";
    yield "Step 3";
  } catch (error) {
    console.log("Caught error:", error.message);
    yield "Error handled";
  }
  
  yield "Continuing after error";
}

const gen = errorHandlingGenerator();

console.log(gen.next());           // { value: "Step 1", done: false }
console.log(gen.next());           // { value: "Step 2", done: false }
console.log(gen.throw(new Error("Something went wrong")));
// "Caught error: Something went wrong" -> { value: "Error handled", done: false }
console.log(gen.next());           // { value: "Continuing after error", done: false }

Practical Generator Examples 🌍

Example 1: Infinite Sequence Generators

// Infinite counter
function* counter(start = 0, step = 1) {
  let current = start;
  while (true) {
    yield current;
    current += step;
  }
}

// Infinite random number generator
function* randomNumbers(min = 0, max = 1) {
  while (true) {
    yield Math.random() * (max - min) + min;
  }
}

// Infinite ID generator
function* idGenerator(prefix = "id") {
  let counter = 1;
  while (true) {
    yield `${prefix}-${counter++}`;
  }
}

// Usage with controlled iteration
const count = counter(0, 2);
for (let i = 0; i < 5; i++) {
  console.log(count.next().value); // 0, 2, 4, 6, 8
}

const ids = idGenerator("user");
console.log(ids.next().value); // "user-1"
console.log(ids.next().value); // "user-2"
console.log(ids.next().value); // "user-3"

// Take first N values from infinite sequence
function take(generator, n) {
  const result = [];
  for (let i = 0; i < n; i++) {
    const { value, done } = generator.next();
    if (done) break;
    result.push(value);
  }
  return result;
}

const randomGen = randomNumbers(1, 10);
console.log(take(randomGen, 5)); // [random numbers between 1-10]

Example 2: Data Processing Pipeline

// Generator-based data processing pipeline
function* filterGenerator(iterable, predicate) {
  for (let item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapGenerator(iterable, transform) {
  for (let item of iterable) {
    yield transform(item);
  }
}

function* takeWhileGenerator(iterable, predicate) {
  for (let item of iterable) {
    if (predicate(item)) {
      yield item;
    } else {
      break;
    }
  }
}

function* chunkGenerator(iterable, size) {
  let chunk = [];
  for (let item of iterable) {
    chunk.push(item);
    if (chunk.length === size) {
      yield chunk;
      chunk = [];
    }
  }
  if (chunk.length > 0) {
    yield chunk;
  }
}

// Create a data processing pipeline
function* processNumbers(numbers) {
  const filtered = filterGenerator(numbers, x => x > 0);
  const squared = mapGenerator(filtered, x => x * x);
  const underHundred = takeWhileGenerator(squared, x => x < 100);
  const chunked = chunkGenerator(underHundred, 3);
  
  yield* chunked;
}

const data = [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const processed = [...processNumbers(data)];
console.log(processed); // [[1, 4, 9], [16, 25, 36], [49, 64, 81]]

Example 3: Asynchronous Generator Pattern

// Simulating async operations with generators
function* asyncTaskSimulator() {
  console.log("Starting async task simulation...");
  
  // Simulate API call 1
  yield delay(1000).then(() => "First API response");
  
  // Simulate API call 2  
  yield delay(500).then(() => "Second API response");
  
  // Simulate processing
  yield delay(800).then(() => "Processing complete");
  
  return "All tasks finished!";
}

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Function to run async generator
async function runAsyncGenerator(gen) {
  let result = gen.next();
  
  while (!result.done) {
    try {
      const value = await result.value;
      console.log("Resolved:", value);
      result = gen.next(value);
    } catch (error) {
      console.error("Error:", error);
      result = gen.throw(error);
    }
  }
  
  console.log("Final result:", result.value);
  return result.value;
}

// Usage
const asyncGen = asyncTaskSimulator();
// runAsyncGenerator(asyncGen);

Advanced Generator Patterns 🚀

Generator Composition and Delegation 🔗

// Generator composition using yield*
function* nums() {
  yield 1;
  yield 2;
  yield 3;
}

function* letters() {
  yield 'a';
  yield 'b';
  yield 'c';
}

function* symbols() {
  yield '!';
  yield '@';
  yield '#';
}

function* combined() {
  yield* nums();    // Delegate to nums generator
  yield* letters(); // Delegate to letters generator
  yield* symbols(); // Delegate to symbols generator
}

console.log([...combined()]); // [1, 2, 3, 'a', 'b', 'c', '!', '@', '#']

// Advanced composition with transformation
function* transformAndCombine() {
  // Transform numbers
  function* doubledNums() {
    for (let num of nums()) {
      yield num * 2;
    }
  }
  
  // Transform letters
  function* uppercaseLetters() {
    for (let letter of letters()) {
      yield letter.toUpperCase();
    }
  }
  
  yield* doubledNums();
  yield "-separator-";
  yield* uppercaseLetters();
}

console.log([...transformAndCombine()]); // [2, 4, 6, "-separator-", "A", "B", "C"]

State Machine with Generators 🎰

// Traffic light state machine
function* trafficLightStateMachine() {
  while (true) {
    console.log("🔴 RED - Stop");
    yield "red";
    
    console.log("🟢 GREEN - Go");  
    yield "green";
    
    console.log("🟡 YELLOW - Caution");
    yield "yellow";
  }
}

// Controlled state machine execution
class TrafficLight {
  constructor() {
    this.stateGenerator = trafficLightStateMachine();
    this.currentState = "red";
    this.timers = new Map();
  }
  
  nextState() {
    const { value } = this.stateGenerator.next();
    this.currentState = value;
    return value;
  }
  
  start() {
    const durations = { red: 3000, green: 5000, yellow: 2000 };
    
    const cycle = () => {
      this.nextState();
      const duration = durations[this.currentState];
      setTimeout(cycle, duration);
    };
    
    cycle();
  }
  
  getCurrentState() {
    return this.currentState;
  }
}

// Usage
const trafficLight = new TrafficLight();
// trafficLight.start(); // Starts the automatic cycling

Cooperative Multitasking 🤝

// Task scheduler using generators
class TaskScheduler {
  constructor() {
    this.tasks = [];
    this.running = false;
  }
  
  addTask(taskGenerator) {
    this.tasks.push(taskGenerator);
  }
  
  start() {
    if (this.running) return;
    this.running = true;
    this.runTasks();
  }
  
  stop() {
    this.running = false;
  }
  
  runTasks() {
    if (!this.running || this.tasks.length === 0) return;
    
    // Give each task a chance to run
    for (let i = this.tasks.length - 1; i >= 0; i--) {
      const task = this.tasks[i];
      const { done } = task.next();
      
      // Remove completed tasks
      if (done) {
        this.tasks.splice(i, 1);
      }
    }
    
    // Schedule next round
    setTimeout(() => this.runTasks(), 10);
  }
}

// Example tasks
function* countingTask(name, max) {
  for (let i = 1; i <= max; i++) {
    console.log(`${name}: ${i}`);
    yield; // Yield control back to scheduler
  }
  console.log(`${name}: completed!`);
}

function* timerTask(name, seconds) {
  const start = Date.now();
  while (Date.now() - start < seconds * 1000) {
    const elapsed = Math.floor((Date.now() - start) / 1000);
    console.log(`${name}: ${elapsed}s elapsed`);
    yield;
  }
  console.log(`${name}: timer finished!`);
}

// Usage
const scheduler = new TaskScheduler();
scheduler.addTask(countingTask("Counter1", 5));
scheduler.addTask(countingTask("Counter2", 3));
scheduler.addTask(timerTask("Timer", 2));
// scheduler.start();

Performance and Memory Considerations 📊

Lazy Evaluation Benefits 💡

// Memory-efficient data processing with generators
function* readLargeFile(filename) {
  // Simulate reading a large file line by line
  const lines = [
    "Line 1: Lorem ipsum dolor sit amet",
    "Line 2: consectetur adipiscing elit", 
    "Line 3: sed do eiusmod tempor incididunt",
    "Line 4: ut labore et dolore magna aliqua",
    "Line 5: Ut enim ad minim veniam",
    // ... potentially millions of lines
  ];
  
  for (let line of lines) {
    yield line;
  }
}

function* processLines(lineGenerator) {
  for (let line of lineGenerator) {
    // Process each line individually
    if (line.includes("Lorem")) {
      yield line.toUpperCase();
    } else if (line.includes("consectetur")) {
      yield line.split("").reverse().join("");
    } else {
      yield line;
    }
  }
}

// Memory efficient: only one line in memory at a time
function processLargeFileEfficiently(filename) {
  const results = [];
  const lineReader = readLargeFile(filename);
  const processor = processLines(lineReader);
  
  // Process first 3 lines only (early termination)
  for (let i = 0; i < 3; i++) {
    const { value, done } = processor.next();
    if (done) break;
    results.push(value);
  }
  
  return results;
}

const processed = processLargeFileEfficiently("large-file.txt");
console.log(processed);
// [
//   "LINE 1: LOREM IPSUM DOLOR SIT AMET",
//   "tile gnicsipida rutetcesnoc :2 eniL", 
//   "Line 3: sed do eiusmod tempor incididunt"
// ]

Generator vs Array Performance 🏃‍♂️

// Performance comparison: Generator vs Array
function arrayApproach(n) {
  // Creates entire array in memory
  const numbers = [];
  for (let i = 0; i < n; i++) {
    numbers.push(i * i);
  }
  
  return numbers
    .filter(x => x % 2 === 0)
    .map(x => x / 2)
    .slice(0, 10);
}

function* generatorApproach(n) {
  for (let i = 0; i < n; i++) {
    const square = i * i;
    if (square % 2 === 0) {
      yield square / 2;
    }
  }
}

function generatorWithEarlyTermination(n) {
  const result = [];
  const gen = generatorApproach(n);
  
  for (let value of gen) {
    result.push(value);
    if (result.length >= 10) break; // Early termination
  }
  
  return result;
}

// Performance test
console.time("Array approach");
const arrayResult = arrayApproach(1000000);
console.timeEnd("Array approach");

console.time("Generator approach");
const genResult = generatorWithEarlyTermination(1000000);
console.timeEnd("Generator approach");

console.log("Results match:", JSON.stringify(arrayResult) === JSON.stringify(genResult));

Common Use Cases and Patterns 🎯

Real-World Applications 🌍

Application 1: Pagination System

class PaginatedDataSource {
  constructor(apiUrl, pageSize = 10) {
    this.apiUrl = apiUrl;
    this.pageSize = pageSize;
  }
  
  // Generator for paginated data
  async* fetchPages() {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
      try {
        const response = await this.fetchPage(page);
        
        if (response.data.length === 0) {
          hasMore = false;
        } else {
          yield {
            page,
            data: response.data,
            total: response.total,
            hasNext: response.hasNext
          };
          
          hasMore = response.hasNext;
          page++;
        }
      } catch (error) {
        console.error(`Error fetching page ${page}:`, error);
        hasMore = false;
      }
    }
  }
  
  async fetchPage(page) {
    // Simulate API call
    return new Promise(resolve => {
      setTimeout(() => {
        const start = (page - 1) * this.pageSize;
        const data = Array.from({ length: this.pageSize }, (_, i) => ({
          id: start + i + 1,
          name: `Item ${start + i + 1}`
        }));
        
        resolve({
          data: page <= 5 ? data : [], // Simulate 5 pages of data
          total: 50,
          hasNext: page < 5
        });
      }, 100);
    });
  }
}

// Usage
async function demonstratePagination() {
  const dataSource = new PaginatedDataSource("/api/items", 10);
  
  for await (let page of dataSource.fetchPages()) {
    console.log(`Page ${page.page}:`, page.data.length, "items");
    
    // Process only first 3 pages
    if (page.page >= 3) break;
  }
}

// demonstratePagination();

Application 2: Event Stream Processing

class EventStream {
  constructor() {
    this.listeners = new Set();
  }
  
  // Generator for event stream
  *events() {
    const eventQueue = [];
    let resolveNext = null;
    
    // Setup event listener
    const listener = (event) => {
      if (resolveNext) {
        resolveNext(event);
        resolveNext = null;
      } else {
        eventQueue.push(event);
      }
    };
    
    this.listeners.add(listener);
    
    try {
      while (true) {
        if (eventQueue.length > 0) {
          yield eventQueue.shift();
        } else {
          // Wait for next event
          yield new Promise(resolve => {
            resolveNext = resolve;
          });
        }
      }
    } finally {
      this.listeners.delete(listener);
    }
  }
  
  emit(eventType, data) {
    const event = { type: eventType, data, timestamp: Date.now() };
    for (let listener of this.listeners) {
      listener(event);
    }
  }
  
  // Filter events by type
  *filterEvents(eventType) {
    for (let event of this.events()) {
      if (event.type === eventType) {
        yield event;
      }
    }
  }
  
  // Transform events
  *mapEvents(transformer) {
    for (let event of this.events()) {
      yield transformer(event);
    }
  }
}

// Usage example
const eventStream = new EventStream();

// Background process emitting events
setTimeout(() => eventStream.emit("user-action", { action: "click", target: "button1" }), 100);
setTimeout(() => eventStream.emit("user-action", { action: "scroll", position: 100 }), 200);
setTimeout(() => eventStream.emit("system-event", { type: "memory-warning" }), 300);
setTimeout(() => eventStream.emit("user-action", { action: "click", target: "button2" }), 400);

// Process user actions only
function processUserActions() {
  const userActions = eventStream.filterEvents("user-action");
  let processedCount = 0;
  
  for (let event of userActions) {
    console.log("User action:", event.data);
    processedCount++;
    
    if (processedCount >= 3) break; // Process first 3 actions
  }
}

// processUserActions();

Summary

Core Concepts

  • Iterator Protocol: Standard interface for making objects iterable
  • Generator Functions: Special functions that can pause and resume execution
  • Lazy Evaluation: Values are produced only when needed
  • Two-Way Communication: Generators can receive values through yield

Key Benefits

  • Memory Efficiency: Process large datasets without loading everything into memory
  • Control Flow: Fine-grained control over execution and iteration
  • Composability: Generators can be easily combined and chained
  • Infinite Sequences: Generate unlimited data on-demand

Common Patterns

  • Data Processing Pipelines: Chain operations without intermediate arrays
  • State Machines: Manage complex state transitions elegantly
  • Async Iteration: Handle asynchronous data streams
  • Cooperative Multitasking: Implement simple schedulers and task managers

Performance Considerations

  • Memory Usage: Generators use constant memory regardless of sequence length
  • Lazy Evaluation: Computation happens only when values are needed
  • Early Termination: Can stop processing when enough results are obtained
  • Function Call Overhead: Generators have slight overhead compared to simple loops

Best Practices

  • Use for large datasets: When memory efficiency matters
  • Implement custom iteration: For complex data structures
  • Chain operations: Build composable data processing pipelines
  • Handle infinite sequences: With proper termination conditions

When to Use

  • Processing large files: Line-by-line or chunk-by-chunk processing
  • API pagination: Fetch data as needed without loading all pages
  • Custom data structures: Trees, graphs, complex collections
  • Event streams: Real-time data processing
  • State management: Complex workflows and state machines

My Personal Insight

Generators fundamentally changed how I think about data processing and control flow. The realization that functions can be pausable and resumable opened up entirely new architectural patterns.

The key breakthrough was understanding that generators aren't just "fancy loops" - they're a powerful abstraction for lazy evaluation and cooperative control flow. They enable elegant solutions to problems that would be complex or memory-intensive with traditional approaches.

Generators taught me that the most powerful abstractions often hide complexity while exposing simple, composable interfaces.

Next Up

Now that you've mastered iterators and generators, we'll explore Modules (import/export) & Dynamic Imports - the modern system for organizing and loading JavaScript code that replaced the chaos of global variables and script tags.

Remember: Iterators and generators aren't just syntax features - they're powerful tools for building memory-efficient, elegant solutions to complex data processing challenges! 🚀✨

Photo of Rahul Aher

Written by Rahul Aher

I'm Rahul, Sr. Software Engineer (SDE II) and passionate content creator. Sharing my expertise in software development to assist learners.

More about me