JS

JavaScript Fundamentals

25 lessons

Progress0%
1. Introduction to JavaScript
1What is JavaScript?2Setting Up Your Environment
2. Variables and Data Types
1Declaring Variables2Data Types3Type Conversion
3. Operators
Arithmetic OperatorsComparison OperatorsLogical Operators
4. Control Flow
Conditional StatementsLoops
5. Functions
Function Basics
6. Arrays & Iteration
Array MethodsSpread, Rest & Destructuring
7. Objects & JSON
Working with ObjectsJSON & Optional Chaining
8. OOP & Classes
Class BasicsInheritance & Private Fields
9. Modules & Modern JS
ES ModulesModern JavaScript Features
10. Async JavaScript
PromisesAsync/Await
11. Error Handling
Error Types & try/catchCustom Errors & Debugging
12. Iterators & Advanced
Iterators & GeneratorsMap, Set & WeakRefs
All Tutorials
JavaScriptIterators & Advanced
Lesson 24 of 25 min
Chapter 12 · Lesson 1

Iterators & Generators

Iterators & Generators

JavaScript's iteration protocol is the mechanism behind for...of, spread syntax, and destructuring. Generators extend that protocol with the ability to produce values lazily, one at a time.

The Iteration Protocol

An object is iterable if it has a [Symbol.iterator] method that returns an iterator — an object with a next() method that returns { value, done }.

js
const iterable = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        return i < 3 ? { value: i++, done: false }
                     : { value: undefined, done: true };
      }
    };
  }
};
[...iterable]; // [0, 1, 2]

for...of

for...of works with any iterable: arrays, strings, Sets, Maps, generators, and your own custom iterables.

js
for (const char of "hello") console.log(char);

Do not confuse for...of (values from an iterable) with for...in (enumerable keys of an object).

Generator Functions

A generator function is declared with function* and can yield multiple values over time:

js
function* count(from, to) {
  for (let i = from; i <= to; i++) {
    yield i;
  }
}
const gen = count(1, 3);
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }

Why Generators?

  • Lazy evaluation — values are computed only when requested
  • Infinite sequences — generate numbers, IDs, or events without holding all in memory
  • Pausable execution — control flow can be suspended and resumed

yield*

yield* delegates to another iterable, useful for composing generators:

js
function* concat(...iterables) {
  for (const it of iterables) yield* it;
}

Code Examples

Custom Iterable with Symbol.iteratorjavascript
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end   = end;
    this.step  = step;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const { end, step } = this;
    return {
      next() {
        if (current <= end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const range = new Range(1, 10, 2);

// Works with for...of
for (const n of range) process.stdout.write(n + " ");
console.log();

// Works with spread
console.log([...new Range(0, 6, 3)]);

// Works with destructuring
const [first, second, third] = new Range(10, 50, 10);
console.log(first, second, third);

Implementing Symbol.iterator makes any object work with for...of, spread, and destructuring transparently. The iterator maintains its own state (current) independently of the iterable.

Generator Functionsjavascript
// Infinite sequence generator (lazy!)
function* naturals(start = 1) {
  let n = start;
  while (true) yield n++;
}

// Take first N values from any iterable
function take(iterable, n) {
  const result = [];
  for (const val of iterable) {
    result.push(val);
    if (result.length >= n) break;
  }
  return result;
}

console.log("First 5 naturals:", take(naturals(), 5));
console.log("Naturals from 10:", take(naturals(10), 5));

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

console.log("First 8 Fibonacci:", take(fibonacci(), 8));

// Generator with return value
function* countdown(from) {
  while (from > 0) yield from--;
  return "liftoff!";
}

const cd = countdown(3);
let result;
while (!(result = cd.next()).done) {
  console.log(result.value);
}
console.log(result.value); // the return value

Generators are lazy — they compute values only when next() is called. An infinite generator is safe because we control when to stop. The return value appears in the final { value, done: true } object.

yield* and Generator Compositionjavascript
function* range(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

// yield* delegates to another iterable
function* concat(...iterables) {
  for (const it of iterables) yield* it;
}

console.log([...concat(range(1, 3), range(7, 9), [20, 21])]);

// Generator pipeline: map + filter lazily
function* map(iter, fn) {
  for (const val of iter) yield fn(val);
}

function* filter(iter, pred) {
  for (const val of iter) if (pred(val)) yield val;
}

// Squares of even numbers from 1–20, lazily computed
const pipeline = filter(
  map(range(1, 20), (n) => n * n),
  (n) => n % 2 === 0
);

console.log([...pipeline]);

yield* delegates iteration to another iterable in-line. Generator pipelines compose map/filter lazily — each value flows through the entire pipeline before the next one is computed, with no intermediate arrays allocated.

Quick Quiz

1. What method must an object implement to be considered iterable?

2. What does a generator function return when called?

3. What is the key advantage of lazy generators over arrays for large sequences?

Was this lesson helpful?

PreviousNext