JavaScript · Syntax · Advanced

Iterators and generators

10 tasks

Build custom iterable objects using `Symbol.iterator`, write generator functions with `function*` and `yield`, and use `yield*` to delegate.

Iterator protocol, lazy evaluation, and generator functions

#
**Why iterators exist** Arrays are eager — they hold all values in memory at once. Iterators are lazy — they produce one value at a time, on demand. This matters when dealing with large datasets, infinite sequences, or expensive computations where you only need some values: ```js // Array: all 1 000 000 numbers in memory at once const arr = Array.from({ length: 1_000_000 }, (_, i) => i); // Generator: produces one number per .next() call, uses almost no memory function* range(n) { for (let i = 0; i < n; i++) yield i; } for (const x of range(1_000_000)) { /* ... */ } ``` **The iterator protocol** An object is *iterable* if it has a `[Symbol.iterator]()` method that returns an *iterator*. An iterator is an object with a `next()` method returning `{ value, done }`: ```js // Manual custom iterator — verbose but shows the mechanism: const counter = { [Symbol.iterator]() { let n = 1; return { next() { return n <= 3 ? { value: n++, done: false } : { value: undefined, done: true }; } }; } }; for (const x of counter) console.log(x); // 1 2 3 console.log([...counter]); // [1, 2, 3] ``` Built-in iterables: `Array`, `String`, `Map`, `Set`, `arguments`, `NodeList`. **Generator functions — the easier way** Writing iterators manually is repetitive. Generator functions (`function*`) let you write the same logic with much less code. Each `yield` pauses execution and sends a value out; calling `.next()` (or `for...of`) resumes from where it paused: ```js function* counter(start, end) { for (let i = start; i <= end; i++) { yield i; // pause here, send i out } // implicit return: { value: undefined, done: true } } for (const n of counter(1, 3)) console.log(n); // 1 2 3 console.log([...counter(1, 5)]); // [1, 2, 3, 4, 5] // Manual .next() calls: const g = counter(1, 2); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: undefined, done: true } ``` A generator function returns a *generator object* — it is both iterable and an iterator, so it works with `for...of`, spread, destructuring, and direct `.next()` calls.

yield*, sending values with .next(), async generators

#
**`yield*` — delegation to another iterable** ```js function* abc() { yield 'a'; yield 'b'; yield 'c'; } function* nums() { yield 1; yield 2; } function* combined() { yield* abc(); // delegates: yields 'a', 'b', 'c' yield* nums(); // then yields 1, 2 yield* [3, 4]; // works with any iterable } console.log([...combined()]); // ['a','b','c',1,2,3,4] ``` **Generator class method — making objects iterable** ```js class Tree { constructor(value, children = []) { this.value = value; this.children = children; } *[Symbol.iterator]() { yield this.value; for (const child of this.children) { yield* child; // recursive delegation } } } const tree = new Tree(1, [new Tree(2, [new Tree(4)]), new Tree(3)]); console.log([...tree]); // [1, 2, 4, 3] (depth-first) ``` **Sending values into a generator with `.next(value)`** The value passed to `.next(value)` becomes the *result* of the `yield` expression inside the generator. This enables two-way communication: ```js function* accumulator() { let total = 0; while (true) { const n = yield total; // yield sends total out; receives next .next(n) if (n === null) return total; total += n; } } const acc = accumulator(); acc.next(); // start: { value: 0, done: false } acc.next(10); // { value: 10, done: false } acc.next(20); // { value: 30, done: false } acc.next(null);// { value: 30, done: true } ``` **Async generators — paginated APIs, streams** Combine `async function*` with `for await...of` to consume async data sources one item at a time without loading everything into memory: ```js async function* paginate(url) { let page = 1; while (true) { const res = await fetch(`${url}?page=${page++}`); const json = await res.json(); if (!json.items.length) break; yield* json.items; } } for await (const user of paginate('/api/users')) { console.log(user.name); } ```

Iterator/Iterable/Generator table, consumers, infinite sequences, early termination

#
**Iterator vs Iterable vs Generator** | Concept | Has | Used with | |---|---|---| | Iterable | `[Symbol.iterator]()` | `for...of`, spread, destructuring | | Iterator | `.next()` → `{value, done}` | `.next()` calls directly | | Generator | both (it IS an iterator + iterable) | both of the above | **Places where iterables are consumed** ```js for (const x of iter) {} // for...of const arr = [...iter]; // spread const [a, b, ...rest] = iter; // destructuring Array.from(iter) // Array.from new Set(iter) / new Map(iter) // Set/Map constructors Promise.all(iter) // Promise combinators ``` **Infinite generators — produce values forever** Since generators are lazy, they can describe infinite sequences. Take only what you need with early `break`: ```js function* naturals(start = 1) { while (true) yield start++; } function* take(n, iterable) { let count = 0; for (const x of iterable) { if (count++ >= n) return; yield x; } } console.log([...take(5, naturals())]); // [1, 2, 3, 4, 5] ``` **Early termination: `return()` and `finally`** When a consumer stops early (`break`, `return`, `.return()`), the generator's `finally` block runs — useful for cleanup: ```js function* withCleanup() { try { yield 1; yield 2; yield 3; } finally { console.log('cleanup!'); } } const g = withCleanup(); g.next(); // { value: 1, done: false } g.return('end'); // 'cleanup!' -> { value: 'end', done: true } // for...of also triggers cleanup on break: for (const x of withCleanup()) { if (x === 1) break; // 'cleanup!' printed } ``` **Generator vs Array — when to use which** | Situation | Use | |---|---| | All values needed immediately | Array | | Large/infinite data, consume partially | Generator | | Async data source (API pages, streams) | Async generator | | Need `.map`/`.filter`/`.reduce` | Array (or convert with `Array.from`) |
01

Implement the iterator protocol

#

Create a `Counter` object that implements the iterator protocol manually. It should count from `start` to `end` (inclusive). Add a `[Symbol.iterator]()` method that returns an object with a `next()` method. Use it in a `for...of` loop.

function makeCounter(start, end) {
  return {
    [Symbol.iterator]() {
      // return iterator object with next()
    }
  };
}

for (const n of makeCounter(1, 5)) {
  console.log(n);  // 1 2 3 4 5
}
Solution
function makeCounter(start, end) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          if (current <= end) {
            return { value: current++, done: false };
          }
          return { value: undefined, done: true };
        }
      };
    }
  };
}

for (const n of makeCounter(1, 5)) {
  console.log(n);  // 1 2 3 4 5
}
02

Generator function with function*

#

Write a generator function `range(start, end, step = 1)` that yields numbers from `start` up to (but not including) `end`, incrementing by `step`. Use it to print numbers 0, 2, 4, 6, 8.

function* range(start, end, step = 1) {
  // yield numbers here
}

for (const n of range(0, 10, 2)) {
  console.log(n);  // 0 2 4 6 8
}
Solution
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

for (const n of range(0, 10, 2)) {
  console.log(n);  // 0 2 4 6 8
}
03

Infinite generator: Fibonacci

#

Write an infinite generator `fibonacci()` that yields the Fibonacci sequence indefinitely (0, 1, 1, 2, 3, 5, 8, ...). Use it to print the first 8 Fibonacci numbers by calling `.next()` or breaking out of a `for...of` loop with a counter.

function* fibonacci() {
  // infinite sequence
}

const gen = fibonacci();
for (let i = 0; i < 8; i++) {
  console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
Solution
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const gen = fibonacci();
for (let i = 0; i < 8; i++) {
  console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
04

yield* — delegate to another generator

#

Write two generators: `odds(n)` yields odd numbers 1, 3, 5, ..., up to n; `evens(n)` yields even numbers 2, 4, 6, ..., up to n. Write a third generator `oddsAndEvens(n)` that uses `yield*` to first delegate to `odds(n)` then to `evens(n)`.

function* odds(n) {
  for (let i = 1; i <= n; i += 2) yield i;
}

function* evens(n) {
  for (let i = 2; i <= n; i += 2) yield i;
}

function* oddsAndEvens(n) {
  // use yield* here
}

console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
Solution
function* odds(n) {
  for (let i = 1; i <= n; i += 2) yield i;
}

function* evens(n) {
  for (let i = 2; i <= n; i += 2) yield i;
}

function* oddsAndEvens(n) {
  yield* odds(n);
  yield* evens(n);
}

console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
05

Make a class iterable

#

Create a class `NumberRange` with `constructor(start, end)`. Add `[Symbol.iterator]()` as a generator method so instances work with `for...of`, spread, and destructuring.

class NumberRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    // yield values here
  }
}

const r = new NumberRange(1, 5);
console.log([...r]);        // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b);          // 1 2
for (const n of r) console.log(n);  // 1 2 3 4 5
Solution
class NumberRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

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

const r = new NumberRange(1, 5);
console.log([...r]);        // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b);          // 1 2
for (const n of r) console.log(n);
06

Generator pipeline

#

Build a pipeline of three generators: `naturals()` — infinite: 1, 2, 3, ...; `take(gen, n)` — takes first n values from a generator; `squares(gen)` — maps each value to its square. Print the first 5 perfect squares using `take(squares(naturals()), 5)`.

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(gen, n) {
  // yield first n values from gen
}

function* squares(gen) {
  // yield x*x for each x in gen
}

console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
Solution
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(gen, n) {
  let count = 0;
  for (const val of gen) {
    if (count++ >= n) break;
    yield val;
  }
}

function* squares(gen) {
  for (const x of gen) yield x * x;
}

console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
07

Generator return value

#

Write a generator `countdown(n)` that yields n, n-1, ..., 1, and then returns `'Done!'`. Show that the return value appears in the final `.next()` call as `{ value: 'Done!', done: true }` but is NOT yielded by `for...of`.

function* countdown(n) {
  while (n > 0) yield n--;
  return 'Done!';
}

const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'Done!', done: true }

// for...of ignores the return value:
for (const n of countdown(3)) {
  process.stdout.write(n + ' ');  // 3 2 1
}
Solution
function* countdown(n) {
  while (n > 0) yield n--;
  return 'Done!';
}

const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'Done!', done: true }

for (const n of countdown(3)) {
  process.stdout.write(n + ' ');  // 3 2 1
}
08

Spread and destructuring with custom iterables

#

Create a `LinkedList` class where each node has `value` and `next`. Add `[Symbol.iterator]()` so the list can be spread into an array and destructured. Build a list 1 → 2 → 3 and verify `[...list]` gives `[1, 2, 3]`.

class LinkedList {
  constructor() { this.head = null; }

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  *[Symbol.iterator]() {
    // traverse from head
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]);         // [1, 2, 3]
const [first, second] = list;
console.log(first, second);     // 1 2
Solution
class LinkedList {
  constructor() { this.head = null; }

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  *[Symbol.iterator]() {
    let node = this.head;
    while (node) {
      yield node.value;
      node = node.next;
    }
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]);         // [1, 2, 3]
const [first, second] = list;
console.log(first, second);     // 1 2
09

Async generator with for await...of

#

Write an async generator `asyncRange(start, end, delay)` that yields numbers from `start` to `end`, waiting `delay` ms between each. Consume it with `for await...of` inside an async `main()` function.

async function* asyncRange(start, end, delay) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, delay));
    yield i;
  }
}

async function main() {
  // use for await...of here
}

main();
Solution
async function* asyncRange(start, end, delay) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, delay));
    yield i;
  }
}

async function main() {
  for await (const n of asyncRange(1, 4, 50)) {
    console.log(n);  // 1 2 3 4 (50ms apart)
  }
}

main();
10

Add Symbol.iterator to a plain object

#

Given `const deck = { suits: ['♠','♥','♦','♣'], values: ['A','2','3'] }`, add a `[Symbol.iterator]` generator method that yields all card combinations (`'A♠'`, `'A♥'`, ..., `'3♣'`). Verify with `[...deck]`.

const deck = {
  suits: ['spade', 'heart', 'diamond', 'club'],
  values: ['A', '2', '3'],
  *[Symbol.iterator]() {
    // yield all value+suit combos
  }
};

console.log([...deck].length);   // 12
console.log([...deck][0]);        // 'A spade'
console.log([...deck][11]);       // '3 club'
Solution
const deck = {
  suits: ['spade', 'heart', 'diamond', 'club'],
  values: ['A', '2', '3'],
  *[Symbol.iterator]() {
    for (const value of this.values) {
      for (const suit of this.suits) {
        yield `${value} ${suit}`;
      }
    }
  }
};

console.log([...deck].length);   // 12
console.log([...deck][0]);        // 'A spade'
console.log([...deck][11]);       // '3 club'