Iterable objects are a generalization of arrays. That’s a concept that allows us to make any object useable in a for..of loop.
Of course, Arrays are iterable. But there are many other built-in objects, that are iterable as well. For instance, strings are also iterable.
If an object isn’t technically an array, but represents a collection (list, set) of something, then for..of is a great syntax to loop over it, so let’s see how to make it work.
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
};
// We want the for..of to work:
// for(let num of range) ... num=1,2,3,4,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..ofstarts, it calls that method once (or errors if not found). The method must return an iterator – an object with the methodnext. - Onward,
for..ofworks only with that returned object. - When
for..ofwants the next value, it callsnext()on that object. - The result of
next()must have the form{done: Boolean, value: any}, wheredone=truemeans that the loop is finished, otherwisevalueis 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
rangeitself 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.
Technically, we may merge them and use range itself as the iterator to make the code simpler.
Like this:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, then 2, 3, 4, 5
}
Now range[Symbol.iterator]() returns the range object itself: it has the necessary next() method and remembers the current iteration progress in this.current. Shorter? Yes. And sometimes that’s fine too.
The downside is that now it’s impossible to have two for..of loops running over the object simultaneously: they’ll share the iteration state, because there’s only one iterator – the object itself. But two parallel for-ofs is a rare thing, even in async scenarios.