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.
// โ 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.
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:
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)
// Count from 1 to 5
for (let i = 1; i <= 5; i++) {
console.log(i);
}โถ Expected Output:
1
2
3
4
5Step-by-step trace:
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 โ STOPLoop through an array by index:
const fruits = ["Apple", "Banana", "Mango"];
for (let i = 0; i < fruits.length; i++) {
console.log(i + ": " + fruits[i]);
}โถ Expected Output:
0: Apple
1: Banana
2: MangoCount backwards:
for (let i = 5; i >= 1; i--) {
console.log(i);
}
// 5, 4, 3, 2, 1Count in steps:
for (let i = 0; i <= 10; i += 2) {
console.log(i); // 0, 2, 4, 6, 8, 10
}๐ก TIP: Use
letfor the loop variable ยท nevervar. Withvar, the variable "leaks" out of the loop block. Withlet, it is properly block-scoped.
๐ COMMON MISTAKE ยท Off-by-one error:
// โ 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.
while (condition) {
// body
}let count = 1;
while (count <= 5) {
console.log(count);
count++;
}โถ Expected Output:
1
2
3
4
5Real example ยท keep asking for input until valid:
// 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.// โ 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.
do {
// body runs first, THEN condition is checked
} while (condition);let count = 1;
do {
console.log(count);
count++;
} while (count <= 5);โถ Expected Output:
1
2
3
4
5The key difference ยท runs at least once even if condition starts false:
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...whileis 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.
for (const key in object) {
// key is each property name (string)
}const student = {
name: "Amara",
score: 85,
grade: "A",
city: "Lagos"
};
for (const key in student) {
console.log(key + ": " + student[key]);
}โถ Expected Output:
name: Amara
score: 85
grade: A
city: LagoshasOwnProperty ยท Skip Inherited Keys:
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...inon arrays!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...inis for objects.for...ofis 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.
for (const value of iterable) {
// value is each element
}Arrays:
const fruits = ["Apple", "Banana", "Mango"];
for (const fruit of fruits) {
console.log(fruit);
}
// Apple
// Banana
// MangoStrings ยท character by character:
for (const char of "Hello") {
console.log(char);
}
// H
// e
// l
// l
// oSets:
const tags = new Set(["js", "web", "css"]);
for (const tag of tags) {
console.log(tag);
}
// js
// web
// cssMaps ยท destructuring key and value:
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, useentries():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:
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: 7continue ยท Skip to next iteration:
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
// 5Labelled break ยท Break out of nested loops:
// 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:
0 0
0 1
0 2
1 02G ยท Loop Comparison Table
| Loop | Use When | Needs index? | Works on objects? | Works on iterables? |
|---|---|---|---|---|
for | Known count / index control | โ Yes | โ (manually) | โ (manually) |
while | Unknown count, condition-driven | โ๏ธ Manual | โ๏ธ Manual | โ๏ธ Manual |
do...while | Run at least once | โ๏ธ Manual | โ๏ธ Manual | โ๏ธ Manual |
for...in | Object property keys | โ No (gives key) | โ Yes | โ ๏ธ Not recommended |
for...of | Any iterable values | โ
via .entries() | โ (plain objects) | โ Yes |
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
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:
| Type | Example |
|---|---|
Array | [1, 2, 3] |
String | "hello" |
Set | new Set([1, 2, 3]) |
Map | new Map([["a", 1]]) |
arguments object | Inside functions |
NodeList | document.querySelectorAll("p") |
| Generator objects | Result of calling a generator function |
TypedArray | Int8Array, 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:
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:
10
20
30This is exactly what for...of does automatically:
for (const x of [10, 20, 30]) {
console.log(x); // 10, 20, 30
}Verifying That Something Is Iterable
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:
true
true
true
true
false
false
falseUsing Spread ... and Destructuring ยท They Use the Iterable Protocol Too!
Any iterable works with spread and destructuring ยท not just arrays:
// 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:
[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...ofin 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.
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:
1
2
3
4
5
[1, 2, 3, 4, 5]๐ค THINK ABOUT IT: What would happen if
currentnever reachedlast? Thedone: truewould never be returned, andfor...ofwould loop forever. Always make sure your iterator eventually returns{ done: true }!
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:
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:
{ 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:
| Term | Definition | Has |
|---|---|---|
| Iterable | An object that CAN be iterated | [Symbol.iterator]() method โ returns an iterator |
| Iterator | An 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:
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]():
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:
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
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:
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.
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:
1
2
3
Iterator closed early โ releasing resourcesPhase 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:
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.
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:
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 callmyGenerator(). This is the pausing behaviour. The function runs until it hits ayield, 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:
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:
1
2
3
[1, 2, 3]
1 2 3Generators 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.
// 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:
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 usebreak,.next(), ortake()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:
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
returnvalue in a generator IS available in.next()but is skipped byfor...of.for...ofstops as soon as it seesdone: truewithout 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.
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:
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 noyieldto 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.
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:
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:
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:
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:
// 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
| Concept | Definition | Key Syntax |
|---|---|---|
| Iterable | Object that CAN produce an iterator | Has [Symbol.iterator]() method |
| Iterator | Object that produces values one at a time | Has .next() โ {value, done} |
| Generator function | Function that produces a generator object | function* with yield |
| Generator object | Both an iterator AND an iterable | Returned by calling function* |
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:
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.8Task A ยท Use the Right Loop
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:
--- 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: BSelf-check questions:
- Why should you NOT use
for...into loop over thestudentsarray? - When would
do...whilebe more appropriate thanwhile? - What is the difference between
breakandcontinue?
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:
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 3Task A ยท Paginator Object
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:
โโ 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: 3Self-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:
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
// 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:
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: ASelf-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 callingyieldinside a loop? - What is "lazy evaluation" and why do generators enable it?
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:
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:
- โ 1. Background: What Is Looping and Why Does It Matter?
- โ 2. Topic 1 ยท JavaScript Looping (All Loop Types)
- โ 3. Topic 2 ยท Iterables
- โ 4. Topic 3 ยท Iterators
- โ 5. Topic 4 ยท Generator Functions
- โ 6. Applied Exercises