JavaScript ยท Lesson 17

JavaScript Looping, Iterables, Iterators & Generators: Every Loop ยท The Iterable Protocol ยท Custom Iterators ยท Generator Functions

6 phases  ยท  Build: ๐Ÿ”ต Stage 1 ยท Data Source Generator

๐Ÿ‘‹ Welcome to Lesson 17

Work through each phase in order. Complete the task before unlocking the next. Your Build It project unlocks when all phases are done.

๐Ÿ“š 6 phases๐Ÿ—๏ธ ๐Ÿ”ต Stage 1 ยท Data Source Generator๐ŸŒ GitHub Pages
Phase 1 of 6
1. Background: What Is Looping and Why Does It Matter?

Imagine you are a teacher who needs to print a report card for every student in a class of 500. Without loops you would write the same print instruction 500 times. With a loop, you write it once and the computer repeats it automatically.

javascript
// โŒ Without a loop โ€” impossible to scale
console.log("Report: Alice  โ€” Score: 88");
console.log("Report: Bob    โ€” Score: 92");
// ... 498 more lines!

// โœ… With a loop โ€” one instruction, any number of repetitions
const students = [{ name: "Alice", score: 88 }, { name: "Bob", score: 92 }];
for (const s of students) {
  console.log(`Report: ${s.name.padEnd(6)} โ€” Score: ${s.score}`);
}

A loop is a block of code that repeats until a condition is met. JavaScript has many loop types ยท each designed for a specific situation.

Beyond simple loops, JavaScript has a deeper concept ยท the iteration protocol. This is a set of rules that defines what it means for anything (not just arrays) to be "loop-able". Understanding this protocol unlocks:

  • Iterables ยท objects that can be looped with for...of
  • Iterators ยท objects that control exactly how values are produced one at a time
  • Generators ยท special functions that can pause and resume, producing values lazily on demand

๐Ÿข REAL WORLD: Iteration is everywhere in professional JavaScript ยท reading lines from a file, paginating API results, streaming data, building custom data structures, and implementing pipelines that process millions of records without loading them all into memory at once.



โœ๏ธ Your Task
Practise what you just learned about 1. Background: What Is Looping and Why Does It Matter?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 6
2. Topic 1 ยท JavaScript Looping (All Loop Types)

Phase 1 ยท Conceptual Understanding

JavaScript has seven loop constructs. Each has a specific job. Understanding when to use each is a core professional skill.


2A ยท for Loop ยท Classic Counter Loop

The for loop is the most fundamental loop. Use it when you know exactly how many times you want to loop, or when you need precise control over the counter.

Structure:

javascript
for (initialise; condition; update) {
  // body runs while condition is true
}
  • Initialise ยท runs once before the loop starts (set up counter)
  • Condition ยท checked before each iteration; loop stops when false
  • Update ยท runs after each iteration (advance counter)
javascript
// Count from 1 to 5
for (let i = 1; i <= 5; i++) {
  console.log(i);
}

โ–ถ Expected Output:

code
1
2
3
4
5

Step-by-step trace:

code
Start:   i = 1 โ†’ condition 1 <= 5 โ†’ true  โ†’ body runs โ†’ i becomes 2
         i = 2 โ†’ condition 2 <= 5 โ†’ true  โ†’ body runs โ†’ i becomes 3
         i = 3 โ†’ condition 3 <= 5 โ†’ true  โ†’ body runs โ†’ i becomes 4
         i = 4 โ†’ condition 4 <= 5 โ†’ true  โ†’ body runs โ†’ i becomes 5
         i = 5 โ†’ condition 5 <= 5 โ†’ true  โ†’ body runs โ†’ i becomes 6
         i = 6 โ†’ condition 6 <= 5 โ†’ false โ†’ STOP

Loop through an array by index:

javascript
const fruits = ["Apple", "Banana", "Mango"];

for (let i = 0; i < fruits.length; i++) {
  console.log(i + ": " + fruits[i]);
}

โ–ถ Expected Output:

code
0: Apple
1: Banana
2: Mango

Count backwards:

javascript
for (let i = 5; i >= 1; i--) {
  console.log(i);
}
// 5, 4, 3, 2, 1

Count in steps:

javascript
for (let i = 0; i <= 10; i += 2) {
  console.log(i); // 0, 2, 4, 6, 8, 10
}

๐Ÿ’ก TIP: Use let for the loop variable ยท never var. With var, the variable "leaks" out of the loop block. With let, it is properly block-scoped.

๐Ÿ› COMMON MISTAKE ยท Off-by-one error:

javascript
// โŒ Wrong โ€” misses last element (< vs <=)
for (let i = 0; i < 5; i++) { ... }  // runs 5 times: 0,1,2,3,4

// To loop array by index, always use i < arr.length (not <=)
for (let i = 0; i < arr.length; i++) { ... }  // correct!
// arr.length - 1 is the last valid index

2B ยท while Loop ยท Condition-First Loop

The while loop repeats as long as a condition is true. Use it when you do not know in advance how many iterations you need.

javascript
while (condition) {
  // body
}
javascript
let count = 1;

while (count <= 5) {
  console.log(count);
  count++;
}

โ–ถ Expected Output:

code
1
2
3
4
5

Real example ยท keep asking for input until valid:

javascript
// Simulated user input
const inputs = ["", "abc", "", "42"]; // first valid input is "42"
let idx = 0;

let input = "";
while (input.trim() === "" || isNaN(Number(input))) {
  input = inputs[idx++]; // simulate reading next input
}
console.log("Valid input received:", input); // "42"

โ–ถ Expected Output: Valid input received: 42

โš ๏ธ WATCH OUT ยท Infinite Loop! If the condition never becomes false, the loop runs forever and crashes the program.

javascript
// โŒ DANGER โ€” infinite loop!
let i = 1;
while (i > 0) {   // i always > 0 since we never change it
console.log(i);
// forgot to add: i++
}
// Always make sure the loop variable changes toward the exit condition!

2C ยท do...while Loop ยท Body-First Loop

Like while, but the body runs at least once before the condition is checked.

javascript
do {
  // body runs first, THEN condition is checked
} while (condition);
javascript
let count = 1;

do {
  console.log(count);
  count++;
} while (count <= 5);

โ–ถ Expected Output:

code
1
2
3
4
5

The key difference ยท runs at least once even if condition starts false:

javascript
let i = 10; // condition (i <= 5) is already false

// while โ€” body NEVER runs
while (i <= 5) {
  console.log("while:", i); // skipped entirely
}

// do...while โ€” body runs ONCE before condition is checked
do {
  console.log("do...while:", i); // prints "do...while: 10"
} while (i <= 5);

โ–ถ Expected Output: do...while: 10

๐Ÿข REAL WORLD: do...while is perfect for menu systems and prompts ยท show the menu at least once, then keep showing it until the user picks "Exit".


2D ยท for...in Loop ยท Iterate Object Properties

for...in loops over the enumerable property keys of an object (including inherited ones from the prototype chain). It is designed for plain objects.

javascript
for (const key in object) {
  // key is each property name (string)
}
javascript
const student = {
  name:  "Amara",
  score: 85,
  grade: "A",
  city:  "Lagos"
};

for (const key in student) {
  console.log(key + ": " + student[key]);
}

โ–ถ Expected Output:

code
name: Amara
score: 85
grade: A
city: Lagos

hasOwnProperty ยท Skip Inherited Keys:

javascript
for (const key in student) {
  if (student.hasOwnProperty(key)) {
    // Only own properties, not inherited prototype properties
    console.log(key + ":", student[key]);
  }
}

โš ๏ธ WATCH OUT ยท Do NOT use for...in on arrays!

javascript
const arr = ["Apple", "Banana", "Mango"];

// โŒ for...in on arrays โ€” gives STRING indices, may include extra properties
for (const i in arr) {
console.log(i, typeof i); // "0" string, "1" string, "2" string โ† strings not numbers!
}

// โœ… Use for...of or a regular for loop for arrays instead

for...in is for objects. for...of is for iterables (arrays, strings, Sets, Maps).


2E ยท for...of Loop ยท Iterate Iterable Values (Modern, Preferred)

for...of loops over the values of any iterable ยท arrays, strings, Sets, Maps, generators, and any object implementing the iterable protocol.

javascript
for (const value of iterable) {
  // value is each element
}

Arrays:

javascript
const fruits = ["Apple", "Banana", "Mango"];

for (const fruit of fruits) {
  console.log(fruit);
}
// Apple
// Banana
// Mango

Strings ยท character by character:

javascript
for (const char of "Hello") {
  console.log(char);
}
// H
// e
// l
// l
// o

Sets:

javascript
const tags = new Set(["js", "web", "css"]);

for (const tag of tags) {
  console.log(tag);
}
// js
// web
// css

Maps ยท destructuring key and value:

javascript
const scores = new Map([["Alice", 88], ["Bob", 92]]);

for (const [name, score] of scores) {
  console.log(name + ": " + score);
}
// Alice: 88
// Bob: 92

๐Ÿ’ก TIP: When you need the index while using for...of, use entries():

javascript
const fruits = ["Apple", "Banana", "Mango"];

for (const [index, fruit] of fruits.entries()) {
console.log(index + ": " + fruit);
}
// 0: Apple
// 1: Banana
// 2: Mango

2F ยท break and continue ยท Loop Control

break ยท Exit the loop immediately:

javascript
const nums = [3, 7, 2, 9, 1, 5];

// Find first number greater than 6
for (const n of nums) {
  if (n > 6) {
    console.log("Found:", n);
    break; // stop immediately
  }
}
// Found: 7

continue ยท Skip to next iteration:

javascript
const nums = [1, 2, 3, 4, 5, 6];

// Print only odd numbers
for (const n of nums) {
  if (n % 2 === 0) continue; // skip even numbers
  console.log(n);
}
// 1
// 3
// 5

Labelled break ยท Break out of nested loops:

javascript
// Without label โ€” only breaks the inner loop
outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    if (i === 1 && j === 1) {
      break outer; // exits BOTH loops immediately
    }
    console.log(i, j);
  }
}
// 0 0
// 0 1
// 0 2
// 1 0
// (stops here โ€” break outer exits both loops)

โ–ถ Expected Output:

code
0 0
0 1
0 2
1 0

2G ยท Loop Comparison Table

LoopUse WhenNeeds index?Works on objects?Works on iterables?
forKnown count / index controlโœ… YesโŒ (manually)โœ… (manually)
whileUnknown count, condition-drivenโš™๏ธ Manualโš™๏ธ Manualโš™๏ธ Manual
do...whileRun at least onceโš™๏ธ Manualโš™๏ธ Manualโš™๏ธ Manual
for...inObject property keysโŒ No (gives key)โœ… Yesโš ๏ธ Not recommended
for...ofAny iterable valuesโœ… via .entries()โŒ (plain objects)โœ… Yes


โœ๏ธ Your Task
Practise what you just learned about 2. Topic 1 ยท JavaScript Looping (All Loop Types). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 6
3. Topic 2 ยท Iterables

Phase 1 ยท Conceptual Understanding

You have used for...of to loop over arrays, strings, Sets, and Maps. But how does for...of know how to loop over them? They are all different types of objects! The answer is the iterable protocol.

What Is an Iterable?

An iterable is any object that has a special method named [Symbol.iterator]. This method, when called, returns an iterator ยท an object that produces values one at a time.

Think of it like a vending machine:

  • The vending machine is the iterable (has a mechanism to deliver items)
  • The dispenser button is [Symbol.iterator]() ยท calling it starts the dispensing process
  • Each press of the button is like calling .next() ยท one item comes out
  • When the machine is empty, it signals done
code
Iterable                โ†’   has [Symbol.iterator]() method
[Symbol.iterator]()     โ†’   returns an Iterator
Iterator                โ†’   has .next() method
.next()                 โ†’   returns { value: ..., done: false/true }

Built-in Iterables in JavaScript

These types are all iterable out of the box:

TypeExample
Array[1, 2, 3]
String"hello"
Setnew Set([1, 2, 3])
Mapnew Map([["a", 1]])
arguments objectInside functions
NodeListdocument.querySelectorAll("p")
Generator objectsResult of calling a generator function
TypedArrayInt8Array, Float64Array, etc.

Plain objects {} are NOT iterable by default.


How for...of Uses the Iterable Protocol Internally

When JavaScript sees for (const x of something), it does this behind the scenes:

javascript
const arr = [10, 20, 30];

// What for...of does internally:
const iterator = arr[Symbol.iterator](); // Step 1: get the iterator

let result = iterator.next();            // Step 2: ask for first value
while (!result.done) {                   // Step 3: loop while not done
  const x = result.value;
  console.log(x);                        // Step 4: use the value
  result = iterator.next();             // Step 5: ask for next value
}

โ–ถ Expected Output:

code
10
20
30

This is exactly what for...of does automatically:

javascript
for (const x of [10, 20, 30]) {
  console.log(x); // 10, 20, 30
}

Verifying That Something Is Iterable

javascript
function isIterable(obj) {
  return obj != null && typeof obj[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3]));          // true
console.log(isIterable("hello"));            // true
console.log(isIterable(new Set([1, 2])));    // true
console.log(isIterable(new Map()));          // true
console.log(isIterable({ a: 1 }));          // false โ† plain object!
console.log(isIterable(42));                // false
console.log(isIterable(null));              // false

โ–ถ Expected Output:

code
true
true
true
true
false
false
false

Using Spread ... and Destructuring ยท They Use the Iterable Protocol Too!

Any iterable works with spread and destructuring ยท not just arrays:

javascript
// Spread Set into array
const set = new Set([1, 2, 3]);
const arr = [...set];
console.log(arr); // [1, 2, 3]

// Spread Map
const map = new Map([["a", 1], ["b", 2]]);
console.log([...map]); // [["a",1],["b",2]]

// Spread String
console.log([..."hello"]); // ["h","e","l","l","o"]

// Destructure a Set
const [first, second] = new Set([10, 20, 30]);
console.log(first, second); // 10 20

// Spread into function arguments
function sum(a, b, c) { return a + b + c; }
const nums = [1, 2, 3];
console.log(sum(...nums)); // 6

โ–ถ Expected Output:

code
[1, 2, 3]
[["a",1],["b",2]]
["h","e","l","l","o"]
10 20
6

๐Ÿข REAL WORLD: The iterable protocol is the foundation of modern JavaScript patterns like Array.from(), Promise.all([...]), for...of in React's render loops, and REST/spread patterns throughout every modern codebase.


Making a Plain Object Iterable

A plain object {} is not iterable ยท for...of will throw a TypeError. To make it iterable, you add a [Symbol.iterator] method.

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

  [Symbol.iterator]() {
    let current = this.from;
    const last  = this.to;

    // Return an iterator object
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

// Now range is iterable!
for (const n of range) {
  console.log(n);
}
// 1, 2, 3, 4, 5

// Spread also works!
console.log([...range]); // [1, 2, 3, 4, 5]

โ–ถ Expected Output:

code
1
2
3
4
5
[1, 2, 3, 4, 5]

๐Ÿค” THINK ABOUT IT: What would happen if current never reached last? The done: true would never be returned, and for...of would loop forever. Always make sure your iterator eventually returns { done: true }!



โœ๏ธ Your Task
Practise what you just learned about 3. Topic 2 ยท Iterables. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 6
4. Topic 3 ยท Iterators

Phase 1 ยท Conceptual Understanding

An iterator is an object that implements the iterator protocol:

  • It must have a .next() method
  • .next() must return an object with exactly two properties:

- value ยท the current value (any type) - done ยท a boolean: false if more values remain, true when finished

When done is true, any value provided is ignored by for...of.


Creating an Iterator from Scratch

Let's build a counter iterator step by step:

javascript
function makeCounter(start, end) {
  let current = start;

  return {
    next() {
      if (current <= end) {
        return { value: current++, done: false };
      }
      return { value: undefined, done: true };
    }
  };
}

const counter = makeCounter(1, 4);

console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: 4, done: false }
console.log(counter.next()); // { value: undefined, done: true }
console.log(counter.next()); // { value: undefined, done: true } โ† stays done

โ–ถ Expected Output:

code
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{ value: undefined, done: true }
{ value: undefined, done: true }

๐Ÿ’ก TIP: Notice that once an iterator returns { done: true }, it should continue to return { done: true } for any further .next() calls. This is the convention.


Iterator vs Iterable ยท What Is the Difference?

This is a very common point of confusion:

TermDefinitionHas
IterableAn object that CAN be iterated[Symbol.iterator]() method โ†’ returns an iterator
IteratorAn object that DOES the iterating.next() method โ†’ returns {value, done}

An iterable produces iterators. An iterator produces values.

Arrays are iterable ยท calling arr[Symbol.iterator]() gives you an iterator:

javascript
const arr      = [10, 20, 30];       // Array is ITERABLE
const iterator = arr[Symbol.iterator](); // iterator is the ITERATOR

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Self-Iterating Objects ยท Combining Both Protocols

The cleanest design makes an object both iterable and its own iterator by returning this from [Symbol.iterator]():

javascript
function makeRange(from, to) {
  return {
    current: from,
    last:    to,

    // Makes it ITERABLE
    [Symbol.iterator]() {
      return this; // โ† returns itself as the iterator
    },

    // Makes it an ITERATOR
    next() {
      if (this.current <= this.last) {
        return { value: this.current++, done: false };
      }
      return { value: undefined, done: true };
    }
  };
}

const range = makeRange(1, 5);

// Works with for...of (uses [Symbol.iterator])
for (const n of range) {
  console.log(n);
}
// 1 2 3 4 5

// Also works with spread (uses [Symbol.iterator])
// Note: range is now exhausted โ€” a new range is needed to spread
const range2 = makeRange(1, 3);
console.log([...range2]); // [1, 2, 3]

โ–ถ Expected Output:

code
1
2
3
4
5
[1, 2, 3]

โš ๏ธ WATCH OUT: When an object is its own iterator (returns this), it is consumed ยท once iterated, it cannot be iterated again. A fresh iterable (like an array) returns a new iterator each time [Symbol.iterator]() is called, so you can loop it many times.


Practical Example ยท A Fibonacci Iterator

javascript
function fibIterator(limit) {
  let prev = 0, curr = 1;

  return {
    [Symbol.iterator]() { return this; },

    next() {
      if (prev > limit) return { value: undefined, done: true };
      const value = prev;
      [prev, curr] = [curr, prev + curr]; // advance sequence
      return { value, done: false };
    }
  };
}

// Print Fibonacci numbers up to 100
for (const n of fibIterator(100)) {
  process.stdout.write(n + " ");
}
console.log();
// 0 1 1 2 3 5 8 13 21 34 55 89

// Collect into array
console.log([...fibIterator(50)]); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

โ–ถ Expected Output:

code
0 1 1 2 3 5 8 13 21 34 55 89
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

๐Ÿข REAL WORLD: Iterators are used to represent sequences that are computed on demand ยท paginated API results, rows from a database cursor, tokens from a parser, or any data where you want the next item only when you need it (lazy evaluation).


Iterator Return Method (Optional Cleanup)

Iterators can optionally implement a return(value) method, which is called when a loop exits early (via break or return inside for...of). Use it to release resources.

javascript
function makeCleanupIterator(items) {
  let index = 0;
  return {
    [Symbol.iterator]() { return this; },

    next() {
      if (index < items.length) {
        return { value: items[index++], done: false };
      }
      return { value: undefined, done: true };
    },

    return(value) {
      console.log("Iterator closed early โ€” releasing resources");
      return { value, done: true };
    }
  };
}

for (const item of makeCleanupIterator([1, 2, 3, 4, 5])) {
  console.log(item);
  if (item === 3) break; // triggers return()
}

โ–ถ Expected Output:

code
1
2
3
Iterator closed early โ€” releasing resources


โœ๏ธ Your Task
Practise what you just learned about 4. Topic 3 ยท Iterators. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 6
5. Topic 4 ยท Generator Functions

Phase 1 ยท Conceptual Understanding

Writing iterators manually (as shown above) works ยท but it is verbose and requires careful state management. Generator functions are a much easier way to create iterators. They let you write iterating logic in a natural, top-to-bottom style using the special yield keyword.

The Magic of yield ยท Pause and Resume

A generator function can pause its execution in the middle and resume later from exactly where it left off. Normal functions cannot do this ยท they run start to finish without stopping.

Think of it like a book with bookmarks:

  • Normal function ยท reads the whole chapter at once, then closes the book
  • Generator function ยท reads one page, puts a bookmark in, hands you the page, waits. When you ask for the next page, it opens the book at the bookmark and continues.

Declaring a Generator Function

The function syntax (note the asterisk ) marks a function as a generator:

javascript
function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

Calling a generator function does not run the body. It returns a generator object ยท which is both an iterator AND an iterable.

javascript
function* myGenerator() {
  console.log("Start");
  yield 1;
  console.log("After first yield");
  yield 2;
  console.log("After second yield");
  yield 3;
  console.log("Done");
}

const gen = myGenerator(); // โ† body does NOT run yet!

console.log(gen.next()); // "Start" then { value: 1, done: false }
console.log(gen.next()); // "After first yield" then { value: 2, done: false }
console.log(gen.next()); // "After second yield" then { value: 3, done: false }
console.log(gen.next()); // "Done" then { value: undefined, done: true }

โ–ถ Expected Output:

code
Start
{ value: 1, done: false }
After first yield
{ value: 2, done: false }
After second yield
{ value: 3, done: false }
Done
{ value: undefined, done: true }

๐Ÿค” THINK ABOUT IT: The console.log("Start") only runs when we call .next() the first time ยท not when we call myGenerator(). This is the pausing behaviour. The function runs until it hits a yield, pauses there, and waits for the next .next() call.


Generators Are Both Iterators and Iterables

Generator objects implement both protocols automatically. This means you can use them directly in for...of, spread, and destructuring:

javascript
function* count() {
  yield 1;
  yield 2;
  yield 3;
}

// for...of (uses iterable protocol)
for (const n of count()) {
  console.log(n);
}
// 1, 2, 3

// Spread (uses iterable protocol)
console.log([...count()]); // [1, 2, 3]

// Destructuring
const [a, b, c] = count();
console.log(a, b, c); // 1 2 3

โ–ถ Expected Output:

code
1
2
3
[1, 2, 3]
1 2 3

Generators with Loops ยท Infinite Sequences

Generators are perfect for producing sequences ยท especially infinite ones. Because values are produced lazily (one at a time on demand), you can define an endless sequence without running out of memory.

javascript
// An INFINITE counter โ€” never runs out!
function* counter(start = 0) {
  let n = start;
  while (true) {   // โ† infinite loop is OK in a generator!
    yield n++;
  }
}

const gen = counter(1);

console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// ... you can call .next() as many times as you want

// Take first 5 values using a helper
function take(gen, n) {
  const result = [];
  for (const val of gen) {
    result.push(val);
    if (result.length === n) break;
  }
  return result;
}

console.log(take(counter(10), 5)); // [10, 11, 12, 13, 14]

โ–ถ Expected Output:

code
1
2
3
[10, 11, 12, 13, 14]

โš ๏ธ WATCH OUT: Never do [...counter()] ยท spreading an infinite generator will try to collect all values until memory is exhausted. Always use break, .next(), or take() to limit how many values you consume from an infinite generator.


return in a Generator

If a generator hits a return statement (or the function body ends), it signals done: true:

javascript
function* gen() {
  yield 1;
  yield 2;
  return "finished"; // โ† done: true, value: "finished"
  yield 3;           // โ† NEVER reached
}

const g = gen();
console.log(g.next()); // { value: 1,          done: false }
console.log(g.next()); // { value: 2,          done: false }
console.log(g.next()); // { value: "finished", done: true  }
console.log(g.next()); // { value: undefined,  done: true  }

๐Ÿ’ก TIP: The return value in a generator IS available in .next() but is skipped by for...of. for...of stops as soon as it sees done: true without using the value.


Passing Values INTO a Generator with .next(value)

You can pass a value into a generator through .next(value). This value becomes the result of the yield expression inside the generator.

javascript
function* dialogue() {
  const name    = yield "What is your name?";
  const age     = yield `Hello ${name}! How old are you?`;
  yield `So ${name} is ${age} years old. Got it!`;
}

const chat = dialogue();

console.log(chat.next().value);          // "What is your name?"
console.log(chat.next("Alice").value);   // "Hello Alice! How old are you?"
console.log(chat.next(30).value);        // "So Alice is 30 years old. Got it!"

โ–ถ Expected Output:

code
What is your name?
Hello Alice! How old are you?
So Alice is 30 years old. Got it!

๐Ÿ’ก TIP: The first .next() call cannot pass a value in (there is no yield to receive it yet). Values passed in through .next(val) only apply from the second call onward.


yield* ยท Delegate to Another Generator or Iterable

yield* inside a generator delegates iteration to another iterable or generator, yielding all of its values in sequence.

javascript
function* inner() {
  yield "B";
  yield "C";
}

function* outer() {
  yield "A";
  yield* inner();  // โ† delegates to inner generator
  yield "D";
}

console.log([...outer()]); // ["A", "B", "C", "D"]

โ–ถ Expected Output: ["A", "B", "C", "D"]

yield* works with any iterable ยท arrays, strings, Sets, other generators:

javascript
function* combined() {
  yield* [1, 2, 3];         // yields from array
  yield* "AB";              // yields characters of string
  yield* new Set([4, 5]);   // yields from Set
}

console.log([...combined()]); // [1, 2, 3, "A", "B", 4, 5]

โ–ถ Expected Output: [1, 2, 3, "A", "B", 4, 5]

๐Ÿข REAL WORLD: yield* is used to build tree-traversal generators ยท e.g., yielding all nodes in a nested file system or DOM tree structure depth-first without recursion complexity.


Generator as an Iterable Object Method

You can use generator functions as methods inside objects or classes, making them directly iterable:

javascript
class NumberRange {
  constructor(from, to, step = 1) {
    this.from = from;
    this.to   = to;
    this.step = step;
  }

  // Generator method โ€” makes instances of NumberRange iterable
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n += this.step) {
      yield n;
    }
  }
}

const evens = new NumberRange(2, 10, 2);

for (const n of evens) {
  process.stdout.write(n + " ");
}
console.log();
// 2 4 6 8 10

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

โ–ถ Expected Output:

code
2 4 6 8 10
[1, 2, 3, 4, 5]

Generator vs Manual Iterator ยท Side by Side

Here is the same Fibonacci iterator from Topic 3, now written as a generator:

javascript
// Manual iterator (Topic 3 version) โ€” verbose!
function fibIterator(limit) {
  let prev = 0, curr = 1;
  return {
    [Symbol.iterator]() { return this; },
    next() {
      if (prev > limit) return { value: undefined, done: true };
      const value = prev;
      [prev, curr] = [curr, prev + curr];
      return { value, done: false };
    }
  };
}

// Generator version โ€” clean and simple!
function* fibGenerator(limit) {
  let prev = 0, curr = 1;
  while (prev <= limit) {
    yield prev;
    [prev, curr] = [curr, prev + curr];
  }
}

// Both produce the same output:
console.log([...fibGenerator(100)]); // [0,1,1,2,3,5,8,13,21,34,55,89]

โ–ถ Expected Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

๐Ÿข REAL WORLD: Generators are used in: Redux Saga (managing async side effects in React apps), data streaming (process file lines one at a time), custom pagination (fetch next page only when needed), and any "lazy" sequence where values should be produced on demand.


Summary: Iterator vs Generator vs Iterable

ConceptDefinitionKey Syntax
IterableObject that CAN produce an iteratorHas [Symbol.iterator]() method
IteratorObject that produces values one at a timeHas .next() โ†’ {value, done}
Generator functionFunction that produces a generator objectfunction* with yield
Generator objectBoth an iterator AND an iterableReturned by calling function*


โœ๏ธ Your Task
Practise what you just learned about 5. Topic 4 ยท Generator Functions. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 6 of 6
6. Applied Exercises

Phase 2 ยท Applied Exercises


Exercise 1 ยท Loop Master ๐Ÿ”

Objective: Practice choosing the right loop for each situation.

Scenario: You are building a student results portal. Each task below requires a different loop type.

Warm-up Micro-Demo:

javascript
const scores = [72, 45, 88, 56, 93];
let total = 0;

for (const score of scores) {
  total += score;
}
console.log("Average:", (total / scores.length).toFixed(1)); // 70.8

Task A ยท Use the Right Loop

javascript
const students = [
  { name: "Amara",  score: 85, grade: "B" },
  { name: "Kwame",  score: 92, grade: "A" },
  { name: "Fatima", score: 58, grade: "F" },
  { name: "Emeka",  score: 74, grade: "C" },
  { name: "Priya",  score: 95, grade: "A" },
];

// 1. FOR loop โ€” print each student with rank
console.log("--- Rankings (for loop) ---");
for (let i = 0; i < students.length; i++) {
  console.log(`${i + 1}. ${students[i].name} โ€” ${students[i].score}`);
}

// 2. FOR...OF โ€” sum all scores
let total = 0;
for (const s of students) { total += s.score; }
console.log("\nClass average:", (total / students.length).toFixed(1));

// 3. FOR...IN โ€” list properties of first student object
console.log("\nStudent record fields (for...in):");
for (const key in students[0]) {
  console.log(`  ${key}: ${students[0][key]}`);
}

// 4. WHILE โ€” find first student scoring above 90
let idx = 0;
while (idx < students.length && students[idx].score <= 90) idx++;
const topStudent = idx < students.length ? students[idx] : null;
console.log("\nFirst student above 90:", topStudent?.name ?? "none");

// 5. DO...WHILE โ€” show menu until valid selection
const validChoices = ["A", "B", "C"];
const inputs = ["X", "Q", "B"]; // simulated; "B" is the first valid one
let choice, inputIdx = 0;
do {
  choice = inputs[inputIdx++];
  console.log("Input received:", choice);
} while (!validChoices.includes(choice));
console.log("Valid choice selected:", choice);

Expected Output:

code
--- Rankings (for loop) ---
1. Amara โ€” 85
2. Kwame โ€” 92
3. Fatima โ€” 58
4. Emeka โ€” 74
5. Priya โ€” 95

Class average: 80.8

Student record fields (for...in):
  name: Amara
  score: 85
  grade: B

First student above 90: Kwame

Input received: X
Input received: Q
Input received: B
Valid choice selected: B

Self-check questions:

  • Why should you NOT use for...in to loop over the students array?
  • When would do...while be more appropriate than while?
  • What is the difference between break and continue?

Exercise 2 ยท Custom Iterable Builder ๐Ÿ”ง

Objective: Practice creating a custom iterable object using [Symbol.iterator].

Scenario: Build a paginated data reader that iterates records in pages of a given size.

Warm-up Micro-Demo:

javascript
const simpleRange = {
  [Symbol.iterator]() {
    let n = 1;
    return { next() { return n <= 3 ? { value: n++, done: false } : { value: undefined, done: true }; } };
  }
};

for (const n of simpleRange) process.stdout.write(n + " ");
console.log(); // 1 2 3

Task A ยท Paginator Object

javascript
function createPaginator(records, pageSize) {
  return {
    records,
    pageSize,

    [Symbol.iterator]() {
      let pageNum = 0;
      const { records, pageSize } = this;

      return {
        next() {
          const start = pageNum * pageSize;
          if (start >= records.length) return { value: undefined, done: true };

          const page = {
            pageNumber: pageNum + 1,
            items: records.slice(start, start + pageSize),
            totalPages: Math.ceil(records.length / pageSize)
          };
          pageNum++;
          return { value: page, done: false };
        }
      };
    }
  };
}

const allRecords = [
  "Record A", "Record B", "Record C", "Record D",
  "Record E", "Record F", "Record G"
];

const paginator = createPaginator(allRecords, 3);

for (const page of paginator) {
  console.log(`\nโ”€โ”€ Page ${page.pageNumber}/${page.totalPages} โ”€โ”€`);
  page.items.forEach((item, i) => console.log(`  ${i + 1}. ${item}`));
}

// Can be spread into an array of page objects
const pages = [...createPaginator(allRecords, 3)];
console.log("\nTotal pages:", pages.length);

Expected Output:

code
โ”€โ”€ Page 1/3 โ”€โ”€
  1. Record A
  2. Record B
  3. Record C

โ”€โ”€ Page 2/3 โ”€โ”€
  1. Record D
  2. Record E
  3. Record F

โ”€โ”€ Page 3/3 โ”€โ”€
  1. Record G

Total pages: 3

Self-check questions:

  • Why is [Symbol.iterator] the key that makes an object iterable?
  • What is the difference between the iterable (paginator) and the iterator (object returned from [Symbol.iterator]())?
  • How would you rewrite this using a generator function?

Exercise 3 ยท Generator Workshop โšก

Objective: Practice writing generator functions for real data tasks.

Scenario: Build generators for a data processing pipeline at an analytics company.

Warm-up Micro-Demo:

javascript
function* squares(n) {
  for (let i = 1; i <= n; i++) yield i * i;
}
console.log([...squares(5)]); // [1, 4, 9, 16, 25]

Task A ยท Utility Generators

javascript
// 1. Unique ID generator
function* idGenerator(prefix = "ID") {
  let n = 1;
  while (true) {
    yield `${prefix}-${String(n++).padStart(4, "0")}`;
  }
}

const userId = idGenerator("USR");
console.log(userId.next().value); // USR-0001
console.log(userId.next().value); // USR-0002
console.log(userId.next().value); // USR-0003

// 2. Chunker โ€” split array into fixed-size chunks lazily
function* chunk(array, size) {
  for (let i = 0; i < array.length; i += size) {
    yield array.slice(i, i + size);
  }
}

const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (const batch of chunk(data, 3)) {
  console.log("Batch:", batch);
}
// Batch: [1, 2, 3]
// Batch: [4, 5, 6]
// Batch: [7, 8, 9]
// Batch: [10]

// 3. Filter + map pipeline using generators
function* filterGen(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

function* mapGen(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
  }
}

const scores  = [45, 72, 88, 56, 93, 61, 78, 34, 90];
const passing = filterGen(scores, s => s >= 60);
const graded  = mapGen(passing, s => ({
  score: s,
  grade: s >= 90 ? "A" : s >= 75 ? "B" : "C"
}));

console.log("\nPassing grades:");
for (const g of graded) {
  console.log(`  Score: ${g.score} โ†’ Grade: ${g.grade}`);
}

Expected Output:

code
USR-0001
USR-0002
USR-0003
Batch: [1, 2, 3]
Batch: [4, 5, 6]
Batch: [7, 8, 9]
Batch: [10]

Passing grades:
  Score: 72  โ†’ Grade: C
  Score: 88  โ†’ Grade: B
  Score: 93  โ†’ Grade: A
  Score: 61  โ†’ Grade: C
  Score: 78  โ†’ Grade: B
  Score: 90  โ†’ Grade: A

Self-check questions:

  • Why is an infinite generator (while (true) { yield ... }) safe to use as long as you break or use .next() selectively?
  • How does yield* differ from calling yield inside a loop?
  • What is "lazy evaluation" and why do generators enable it?


โœ๏ธ Your Task
ยท ### Exercise 1 ยท Loop Master ๐Ÿ” Objective: Practice choosing the right loop for each situation. Scenario: You are building a student results portal. Each task below requires a different loop type.
๐Ÿ—๏ธ Build It โ€” Mini Project
๐Ÿ”ต Stage 1 ยท Data Source Generator

Phase 3 ยท Project Simulation

Real-world scenario: You work at a data analytics company. You receive large batches of raw transaction records. Build a lazy data pipeline using generators and iterators that:

  • Reads records lazily (one at a time) without loading the entire dataset
  • Filters, transforms, and enriches data through a composable pipeline
  • Paginates output into batches for display or further processing
  • Tracks pipeline statistics using a custom iterable

๐Ÿ”ต Stage 1 ยท Data Source Generator

Goal: Create a generator that simulates streaming raw records lazily, one at a time.

Simple stage preview:

starter.html
function* range(n) { for (let i = 1; i <= n; i++) yield i; }
console.log([...range(4)]); // [1, 2, 3, 4]

Lesson 17 complete! ๐ŸŽ‰

You covered: