Iterators & Generators

Med

The iteration protocol defines how JavaScript objects become iterable — usable with for...of, spread, destructuring, and Array.from. Any object with a Symbol.iterator method that returns a next() function is iterable. Generator functions (function*) provide a concise way to create iterators using yield, enabling lazy evaluation and infinite sequences.

Interactive Visualization

for...in vs for...of

Step 1: Setup
const obj = { a: 1, b: 2 };
const arr = [10, 20, 30];
Output
No output yet
1 / 5

Key Points

  • An iterator is an object with a next() method that returns { value, done }
  • An iterable is an object with a [Symbol.iterator]() method returning an iterator
  • Generator functions (function*) pause at each yield and resume when next() is called
  • Generators are lazy — they compute values on demand, not all at once
  • yield* delegates to another iterable or generator
  • for...of, spread, destructuring, and Array.from all consume iterables

Code Examples

Custom Iterable Object

const range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    let current = this.from
    const last = this.to
    return {
      next() {
        if (current <= last) return { value: current++, done: false }
        return { value: undefined, done: true }
      },
    }
  },
}

for (const num of range) console.log(num) // 1, 2, 3, 4, 5
const arr = [...range] // [1, 2, 3, 4, 5]

The range object implements Symbol.iterator, making it work with for...of, spread, and destructuring.

Generator Function Basics

function* countdown(n) {
  while (n > 0) {
    yield n
    n--
  }
  yield 'Go!'
}

const counter = countdown(3)
console.log(counter.next()) // { value: 3, done: false }
console.log(counter.next()) // { value: 2, done: false }
console.log(counter.next()) // { value: 1, done: false }
console.log(counter.next()) // { value: 'Go!', done: false }
console.log(counter.next()) // { value: undefined, done: true }

function* creates a generator. Each yield pauses execution. Calling next() resumes from where it paused.

Infinite Sequences with Lazy Evaluation

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

function take(n, iterable) {
  const result = []
  for (const value of iterable) {
    result.push(value)
    if (result.length >= n) break
  }
  return result
}

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

The fibonacci generator produces an infinite sequence but only computes values when requested.

yield* Delegation

function* inner() { yield 'a'; yield 'b' }

function* outer() {
  yield 1
  yield* inner()     // delegates to inner
  yield 2
  yield* [10, 20]    // delegates to any iterable
  yield 3
}

console.log([...outer()]) // [1, 'a', 'b', 2, 10, 20, 3]

// Tree traversal
function* traverse(node) {
  yield node.value
  if (node.left) yield* traverse(node.left)
  if (node.right) yield* traverse(node.right)
}

yield* delegates to another iterable, flattening its values. Powerful for recursive data structures.

Common Mistakes

  • Generators are single-use — once exhausted, next() always returns { done: true }
  • Using return inside a generator — the value is skipped by for...of
  • Not breaking out of infinite generator loops
  • Confusing Symbol.iterator (makes iterable) with the iterator itself (has next())

Interview Tips

  • Implement a custom iterable from scratch — Symbol.iterator and next() pattern
  • Demonstrate lazy evaluation with infinite sequences and explain the memory advantage
  • Generators conform to both iterator and iterable protocols
  • Practical uses: paginated API consumption, tree traversal, streaming data

Related Concepts