JavaScript Iterators
Introduction
Iterators are one of the powerful features introduced in ES6 (ECMAScript 2015) that provide a standard way to access elements in a collection one at a time. They enable you to traverse through data structures like arrays, strings, maps, sets, and even custom objects in a consistent manner.
At their core, iterators follow a simple principle: they provide a method called next()
that returns an object with two properties:
value
: the current elementdone
: a boolean indicating whether iteration is complete
Let's dive deeper into understanding how iterators work and how you can leverage them in your JavaScript applications.
The Iterator Protocol
The iterator protocol defines how to produce a sequence of values from an object. An object becomes an iterator when it implements a next()
method with the correct behavior.
Here's a simple example of a basic iterator:
// Creating a simple iterator
function createCounterIterator(start, end) {
let count = start;
return {
next() {
if (count <= end) {
return { value: count++, done: false };
} else {
return { done: true };
}
}
};
}
// Using the iterator
const counter = createCounterIterator(1, 5);
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: 5, done: false }
console.log(counter.next()); // { done: true }
In this example, we've created a simple counter iterator that counts from a start value to an end value. Each time we call next()
, we get the current count as the value, and the counter increments.
Iterable Objects
An iterable is an object that implements the iterable protocol by having a method with the key Symbol.iterator
that returns an iterator. Many built-in types in JavaScript are already iterable:
- Arrays
- Strings
- Maps
- Sets
- DOM NodeLists
Here's how to create a custom iterable object:
// Creating a custom iterable object
const customIterable = {
data: [10, 20, 30, 40, 50],
// The Symbol.iterator method that returns an iterator
[Symbol.iterator]() {
let index = 0;
return {
// The iterator object with next() method
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
// Using the iterable with for...of
for (const item of customIterable) {
console.log(item);
}
// Output:
// 10
// 20
// 30
// 40
// 50
// Using the spread operator with the iterable
const values = [...customIterable];
console.log(values); // [10, 20, 30, 40, 50]
The for...of
Loop
The for...of
loop was introduced alongside iterators and iterables. It provides a clean way to loop over iterable objects:
const fruits = ['Apple', 'Banana', 'Orange', 'Mango'];
// Using for...of loop
for (const fruit of fruits) {
console.log(fruit);
}
// Output:
// Apple
// Banana
// Orange
// Mango
The for...of
loop calls the Symbol.iterator
method behind the scenes and then repeatedly calls the next()
method on the resulting iterator until the iteration is complete.
Built-in Iterables
Let's look at how iterators work with different built-in types:
String Iteration
const message = "Hello";
for (const char of message) {
console.log(char);
}
// Output:
// H
// e
// l
// l
// o
Map Iteration
const userRoles = new Map([
['John', 'Admin'],
['Sarah', 'Editor'],
['Mike', 'Subscriber']
]);
for (const [user, role] of userRoles) {
console.log(`${user} is a ${role}`);
}
// Output:
// John is a Admin
// Sarah is a Editor
// Mike is a Subscriber
Set Iteration
const uniqueColors = new Set(['red', 'blue', 'green', 'red']);
for (const color of uniqueColors) {
console.log(color);
}
// Output:
// red
// blue
// green
Generator Functions: Simplified Iterators
Generator functions provide an easier way to create iterators. They use the function*
syntax and yield
keyword to define iterative algorithms in a simpler and more readable manner.
function* countUp(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const counter = countUp(1, 5);
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: 5, done: false }
console.log(counter.next()); // { value: undefined, done: true }
// Or using a for...of loop
for (const num of countUp(1, 5)) {
console.log(num);
}
// Output:
// 1
// 2
// 3
// 4
// 5
Practical Examples
Example 1: Creating a Pagination Iterator
function* paginate(array, pageSize) {
for (let i = 0; i < array.length; i += pageSize) {
yield array.slice(i, i + pageSize);
}
}
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
const pages = paginate(data, 4);
console.log(pages.next().value); // [1, 2, 3, 4]
console.log(pages.next().value); // [5, 6, 7, 8]
console.log(pages.next().value); // [9, 10, 11, 12]
console.log(pages.next().done); // true
Example 2: Creating an Iterator for a Tree Structure
// A tree node structure
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
// Create a sample tree
const root = new TreeNode(10);
root.left = new TreeNode(5);
root.right = new TreeNode(15);
root.left.left = new TreeNode(3);
root.left.right = new TreeNode(7);
root.right.left = new TreeNode(12);
root.right.right = new TreeNode(18);
// In-order traversal using generator
function* inOrderTraversal(node) {
if (node) {
yield* inOrderTraversal(node.left);
yield node.value;
yield* inOrderTraversal(node.right);
}
}
// Use the iterator to traverse the tree
for (const value of inOrderTraversal(root)) {
console.log(value);
}
// Output:
// 3
// 5
// 7
// 10
// 12
// 15
// 18
Example 3: Generating an Infinite Sequence
// Generate Fibonacci numbers infinitely
function* fibonacciGenerator() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Get the first 10 Fibonacci numbers
const fib = fibonacciGenerator();
const first10 = [];
for (let i = 0; i < 10; i++) {
first10.push(fib.next().value);
}
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Summary
JavaScript iterators provide a powerful and consistent way to traverse various data structures. They work hand-in-hand with iterables and the for...of
loop to create a standardized iteration protocol in JavaScript.
Key takeaways:
- Iterators implement a
next()
method that returns{ value, done }
- Iterables implement
Symbol.iterator
which returns an iterator - Generator functions provide an easy way to create iterators
- Many built-in objects like arrays, strings, maps, and sets are already iterable
- Iterators enable powerful patterns like lazy evaluation and infinite sequences
Practice Exercises
- Create an iterator that generates all even numbers up to a specified limit.
- Build a custom iterable object that represents a classroom with students.
- Write a generator function that yields the reverse of a given array.
- Create an iterator that traverses through a nested object structure.
- Implement a "range" function similar to Python's
range
using iterators.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)