3.4 Iterables and Iterators
What is the Iteration Protocol?
The Iteration Protocol introduced in ES6 defines a unified way to traverse data. Objects following this protocol can be used with for...of, the spread operator (...), destructuring, and more.
It consists of two protocols:
- Iterable Protocol: Implements the
Symbol.iteratormethod - Iterator Protocol: Returns an object with a
next()method
Iterator Protocol
An iterator has a next() method that returns objects of the form { value, done }.
// Manual iterator usage
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
// for...of handles this internally
for (const item of [1, 2, 3]) {
console.log(item); // 1, 2, 3
}
Built-in Iterables
// Array
for (const x of [1, 2, 3]) console.log(x);
// String
for (const char of "Hello") console.log(char); // H, e, l, l, o
// Map
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// Set
const set = new Set([1, 2, 3]);
for (const x of set) console.log(x);
// NodeList (DOM)
for (const el of document.querySelectorAll("p")) {
el.style.color = "red";
}
Custom Iterable
Implement Symbol.iterator to make any object iterable.
// Range iterable
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 };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]
const [first, second, ...rest] = range;
console.log(first, second, rest); // 1 2 [3, 4, 5]
// Reusable Range class
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 };
}
};
}
}
console.log([...new Range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
Generator Basics
Generators are a simpler way to create iterators. They use the function* syntax and yield keyword.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
for (const x of simpleGenerator()) {
console.log(x); // 1, 2, 3
}
// Range as generator (much simpler!)
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
Generator Execution Flow
Generators pause at yield and resume when next() is called.
function* stepByStep() {
console.log("Step 1 start");
yield "first value";
console.log("Step 2 start");
yield "second value";
console.log("Step 3 start");
return "done";
}
const gen = stepByStep();
console.log(gen.next());
// "Step 1 start"
// { value: "first value", done: false }
console.log(gen.next());
// "Step 2 start"
// { value: "second value", done: false }
console.log(gen.next());
// "Step 3 start"
// { value: "done", done: true }
Infinite Sequences
Generators can produce infinite sequences that generate values endlessly.
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
function take(iterable, n) {
const result = [];
for (const item of iterable) {
result.push(item);
if (result.length >= n) break;
}
return result;
}
console.log(take(naturals(), 10)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Fibonacci sequence
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
console.log(take(fibonacci(), 10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Lazy Evaluation
One of the key benefits of generators is lazy evaluation — values are computed only when needed.
// Eager: all data in memory at once
const bigArray = Array.from({ length: 1000000 }, (_, i) => i * 2);
const filtered = bigArray.filter(n => n % 6 === 0).slice(0, 5);
// Creates 1M items → filters → uses only 5 (wasteful!)
// Lazy: compute only what's needed
function* generate(count) {
for (let i = 0; i < count; i++) {
yield i * 2;
}
}
function* lazyFilter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) yield item;
}
}
function* lazyTake(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count >= n) break;
yield item;
count++;
}
}
// Only computes until finding 5 items!
const lazyResult = [...lazyTake(
lazyFilter(generate(1000000), n => n % 6 === 0),
5
)];
console.log(lazyResult); // [0, 6, 12, 18, 24]
Pro Tips
Pagination Generator
async function* paginate(url) {
let page = 1;
while (true) {
const response = await fetch(`${url}?page=${page}`);
const { data, hasMore } = await response.json();
yield* data;
if (!hasMore) break;
page++;
}
}
// Automatically iterates all pages
for await (const item of paginate('/api/items')) {
console.log(item);
// Process each item; page transitions are automatic
}