Skip to main content
Advertisement

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.iterator method
  • 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
}
Advertisement