Advance Javascript series: Iterables
As a language, JavaScript is built on the concept of iteration. The ability to repeat a task or action over a set of data is fundamental to many of the operations we perform in programming. In JavaScript, the concept of iteration is implemented using the concept of iterables.
Iterables are data structures that allow for sequential access to their elements by other data consumers. For instance, a data structure that unloads data one-by-one in order when placed in a for...of loop.
The iterable protocol consists of the iterable, which is the data structure itself, and the iterator, a pointer that moves over the iterable. When an array is placed in a for...of loop, its iterable property, named Symbol.iterator, returns an iterator. This object can be used on a shared interface used by all looping control structures.
Iterating over arrays
One of the most common uses of iterables in JavaScript is to iterate over arrays. Arrays are collections of data that can be indexed by a numerical index. The following example shows how to iterate over an array of numbers using a for...of
loop:
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
In this example, the for...of
loop iterates over the numbers
array, assigning each element in turn to the number
variable. The loop then logs each number to the console.
Iterating over strings
Strings are another common iterable in JavaScript. A string can be thought of as an array of characters. The following example shows how to iterate over a string using a for...of
loop:
const str = "hello";
for (const char of str) {
console.log(char);
}
In this example, the for...of
loop iterates over the str
string, assigning each character in turn to the char
variable. The loop then logs each character to the console.
Iterating over sets
Sets are a relatively new data structure in JavaScript, added in ECMAScript 6. Sets are similar to arrays in that they are collections of data, but they have some important differences. One of these differences is that sets do not allow duplicates. The following example shows how to iterate over a set using a for...of
loop:
const mySet = new Set([1, 2, 3]);
for (const value of mySet) {
console.log(value);
}
In this example, the for...of
loop iterates over the mySet
set, assigning each value in turn to the value
variable. The loop then logs each value to the console.
Creating custom iterables
While many built-in JavaScript data structures are iterables, it is also possible to create custom iterables. To create a custom iterable, an object must implement the Symbol.iterator
method. This method must return an iterator object that provides a next()
method.
The following example shows how to create a custom iterable that generates the Fibonacci sequence:
const fibonacci = {
[Symbol.iterator]() {
let a = 0, b = 1;
return {
next() {
const value = a;
a = b;
b = value + b;
return { value, done: false };
}
}
}
}
for (const number of fibonacci) {
console.log(number);
if (number > 1000) {
break;
}
}
In this example, the fibonacci
object is a custom iterable that generates the Fibonacci sequence. The Symbol.iterator
method returns an iterator object that generates the sequence. The next()
method of the iterator object returns the next value in the sequence each time it is called.
Symbol.iterator
We can easily grasp the concept of iterables by making one of our own.
For instance, we have an object that is not an array, but looks suitable for for..of
.
Like a range
object that represents an interval of numbers:
let range = {
from: 1,
to: 5
};
To make the range
object iterable (and thus let for..of
work) we need to add a method to the object named Symbol.iterator
(a special built-in symbol just for that).
- When
for..of
starts, it calls that method once (or errors if not found). The method must return an iterator – an object with the methodnext
. - Onward,
for..of
works only with that returned object. - When
for..of
wants the next value, it callsnext()
on that object. - The result of
next()
must have the form{done: Boolean, value: any}
, wheredone=true
means that the loop is finished, otherwisevalue
is the next value.
Here’s the full implementation for range
with remarks:
let range = {
from: 1,
to: 5
};
// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {
// ...it returns the iterator object:
// 2. Onward, for..of works only with the iterator object below, asking it for next values
return {
current: this.from,
last: this.to,
// 3. next() is called on each iteration by the for..of loop
next() {
// 4. it should return the value as an object {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// now it works!
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
Please note the core feature of iterables: separation of concerns.
- The
range
itself does not have thenext()
method. - Instead, another object, a so-called “iterator” is created by the call to
range[Symbol.iterator]()
, and itsnext()
generates values for the iteration.
So, the iterator object is separate from the object it iterates over.