Understanding the JavaScript Iterator Protocol
This article provides a comprehensive overview of the JavaScript
iterator protocol, explaining how JavaScript standardizes the process of
looping over data collections. You will learn the difference between the
iterable and iterator protocols, how the next() method
manages iteration state, and how to implement custom iterators from
scratch using standard JavaScript syntax and generator functions.
What Is the Iterator Protocol?
In JavaScript, the iterator protocol defines a standard way to produce a sequence of values, either finite or infinite.
An object is an iterator when it implements a
next() method. The next() method takes zero or
one argument and must return an object with two specific properties:
value: The current value in the iteration sequence. This can be any JavaScript data type. Whendoneistrue,valuecan be omitted (or set toundefined).done: A boolean indicating whether the sequence has finished. It isfalseif more values can be produced, andtrueif the iterator has reached the end of the sequence.
// Example shape of an iterator's result object
{ value: 42, done: false }
{ value: undefined, done: true }The Iterable Protocol
While the iterator protocol defines how values are retrieved sequentially, the iterable protocol defines how an object makes itself iterable.
An object is iterable if it defines a method with
the key [Symbol.iterator]. This method must return an
object that adheres to the iterator protocol (an iterator).
Built-in iterables in JavaScript include: - Array -
String - Map - Set -
TypedArray
Built-in language constructs that consume iterables include the
for...of loop, the spread operator (...),
Array.from(), and destructuring assignment.
How to Implement a Custom Iterator
To make a custom object both iterable and compatible with JavaScript iteration constructs, implement both the iterable and iterator protocols.
Example: Custom Range Iterator
Below is an implementation of a Range object that steps
through numbers from a start value to an end value:
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
// The Iterable Protocol: define [Symbol.iterator]
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
// The Iterator Protocol: return an object with a next() method
return {
next() {
if (current <= end) {
const result = { value: current, done: false };
current += step;
return result;
}
return { value: undefined, done: true };
}
};
}
}
// Consuming the custom iterable with for...of
const numbers = new Range(1, 10, 2);
for (const num of numbers) {
console.log(num); // Outputs: 1, 3, 5, 7, 9
}
// Consuming using the spread operator
console.log([...numbers]); // Outputs: [1, 3, 5, 7, 9]Manual Consumption with
next()
You can also consume an iterator manually without using
for...of loops:
const range = new Range(1, 3);
const iterator = range[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 }Simplifying Implementation with Generators
Writing custom iterator objects manually requires keeping track of
internal state using closures or properties. Generator
functions (function*) automatically implement the
iterator and iterable protocols, drastically simplifying the code.
class GeneratorRange {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
*[Symbol.iterator]() {
for (let current = this.start; current <= this.end; current += this.step) {
yield current;
}
}
}
const genNumbers = new GeneratorRange(5, 15, 5);
for (const val of genNumbers) {
console.log(val); // Outputs: 5, 10, 15
}When using yield, the JavaScript engine automatically
constructs the { value, done } objects and manages the
execution state of the function.