Learning Objectives

  • Understand what generator functions are
  • Learn the difference between regular and generator functions
  • Explore basic generator syntax
  • Understand when to use generators

What is a Generator?

A generator is a special type of function that can pause its execution and resume later. Unlike regular functions that run to completion, generators can yield multiple values over time.

Regular Function vs Generator

// Regular function - runs to completion
function regularFunction() {
  console.log('Start');
  console.log('Middle');
  console.log('End');
  return 'Done';
}

regularFunction(); // Logs all three, returns 'Done'

// Generator function - can pause and resume
function* generatorFunction() {
  console.log('Start');
  yield 1;
  console.log('Middle');
  yield 2;
  console.log('End');
  return 'Done';
}

const gen = generatorFunction();
gen.next(); // Logs 'Start', returns { value: 1, done: false }
gen.next(); // Logs 'Middle', returns { value: 2, done: false }
gen.next(); // Logs 'End', returns { value: 'Done', done: true }

Generator Syntax

Generators are defined using the function* syntax (note the asterisk):

// Function declaration
function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

// Function expression
const myGen = function* () {
  yield 1;
  yield 2;
};

// Method in object
const obj = {
  *myGenerator() {
    yield 1;
    yield 2;
  }
};

// Class method
class MyClass {
  *myGenerator() {
    yield 1;
    yield 2;
  }
}

How Generators Work

When you call a generator function, it doesn't execute immediately. Instead, it returns a generator object:

function* simpleGenerator() {
  yield 'First';
  yield 'Second';
  yield 'Third';
}

const gen = simpleGenerator();
console.log(gen); // Object [Generator] {}

// Generator object has a next() method
console.log(gen.next()); // { value: 'First', done: false }
console.log(gen.next()); // { value: 'Second', done: false }
console.log(gen.next()); // { value: 'Third', done: false }
console.log(gen.next()); // { value: undefined, done: true }

The yield Keyword

The yield keyword pauses the generator and returns a value:

function* counter() {
  let count = 0;
  
  while (true) {
    yield count;
    count++;
  }
}

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

Generator Return Values

Each call to next() returns an object with two properties:

function* example() {
  yield 1;
  yield 2;
  return 3;
}

const gen = example();

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

Generators are Iterable

Generators implement the iterator protocol, making them compatible with for...of loops:

function* colors() {
  yield 'red';
  yield 'green';
  yield 'blue';
}

// Using for...of
for (const color of colors()) {
  console.log(color);
}
// Output:
// red
// green
// blue

// Using spread operator
const colorArray = [...colors()];
console.log(colorArray); // ['red', 'green', 'blue']

// Using Array.from()
const colorArray2 = Array.from(colors());
console.log(colorArray2); // ['red', 'green', 'blue']

When to Use Generators

1. Lazy Evaluation

// Generate values on demand
function* fibonacci() {
  let [a, b] = [0, 1];
  
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3

2. Memory Efficiency

// Instead of creating a large array
function createLargeArray(n) {
  const arr = [];
  for (let i = 0; i < n; i++) {
    arr.push(i);
  }
  return arr; // Uses lots of memory
}

// Use a generator
function* generateNumbers(n) {
  for (let i = 0; i < n; i++) {
    yield i; // One value at a time
  }
}

// Process one million numbers without storing them all
for (const num of generateNumbers(1000000)) {
  if (num > 10) break; // Can stop early
}

3. Custom Iteration

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
  
  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const range = new Range(1, 5);
console.log([...range]); // [1, 2, 3, 4, 5]

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

Real-World Example: Pagination

function* paginate(items, pageSize) {
  for (let i = 0; i < items.length; i += pageSize) {
    yield items.slice(i, i + pageSize);
  }
}

const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pages = paginate(data, 3);

console.log(pages.next().value); // [1, 2, 3]
console.log(pages.next().value); // [4, 5, 6]
console.log(pages.next().value); // [7, 8, 9]
console.log(pages.next().value); // [10]

Advantages of Generators

Key Takeaways

  • ✅ Generators are functions that can pause and resume
  • ✅ Defined with function* syntax
  • ✅ Use yield to return values
  • ✅ Return generator objects with next() method
  • ✅ Implement the iterator protocol
  • ✅ Perfect for lazy evaluation and infinite sequences

Next Steps

Now that you understand what generators are, let's dive deeper into the iterator protocol!