A New Way to Iterate in ES6 with Iterables and Iterators
- ES6 introduced a new way to iterate:
for-of. for-ofiterates over iterable objects.- The iterable protocol requires an object to implement an iterator method on its
[Symbol.iterator]prototype that returns an object satisfying the iterator protocol in order to be considered iterable. - The iterator protocol requires an object to return a
nextfunction that in turn returns a{value, done}object in order to be considered an iterator.
Just as all roads lead to Rome, JavaScript also has several roads for performing iteration. Some of the most common ones we encounter include for loop, while loop, and do-while loop. Let’s look at an example of iterating over an array below.
const arr = [0, 1, 2, 3, 4, 5];
// classic for loop
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// classic while loop
const j = 0;
while (j < arr.length) {
console.log(arr[j]);
j++;
}
// do-while, somewhat less common
const k = 0;
do {
console.log(arr[k]);
k++;
} while (k < arr.length);
These three classic iteration approaches are often sufficient for our programs. However, ES6 brings a new and quite interesting way to iterate: for-of.
The For-of Loop
Iterating with for-of has a very concise and clean syntax. Still using the same arr array:
for (item of arr) {
console.log(item);
}
It turns out that, beyond arrays, the for-of loop can also be used to iterate over other objects.
const str = "Hello";
for (char of str) {
console.log(char);
} // "H", "e", "l", "l", "o"
const s = new Set(str);
for (val of s) {
console.log(val);
} // "H", "e", "l", "o"
const m = new Map([
[1, 2],
[3, 4],
]);
for (val of m) {
console.log(val);
} // [1, 2], [3, 4]
Besides arrays, this appealing iteration syntax can also be applied to String, Set, and Map objects. Why is that? Let’s look at what MDN says about for-of:
“The
for...ofstatement creates a loop Iterating over iterable objects” - Mozilla Developer Network
It turns out there is a classification of objects called “iterable”, which includes Arrays, Strings, Sets, and Maps. If you come from Java, you might intuitively guess that iterable refers to some interface. However, JavaScript does not have interfaces — what JavaScript recognizes is protocols, and iterable is one of them.
The Iterable Protocol
Protocols in JavaScript are conventions that an object must satisfy in order to make use of certain features of the JavaScript language. The concept of protocols was introduced in ES6 with the iterable and iterator protocols. As described above, objects that satisfy the iterable protocol can harness the power of for-of.
To be considered iterable, an object (or another object in its prototype chain) must implement the @@iterator method — a function that returns an iterator object. That function must be attached to the object’s Symbol.iterator property. This symbol is one of JavaScript’s built-in symbols, specifically pointing to an object’s @@iterator method.
So Arrays, Strings, Sets, and Maps all have an iterator method? That’s exactly right!
const a = [];
typeof a[Symbol.iterator]; // "function"
const s = new Set();
typeof s[Symbol.iterator]; // "function"
const m = new Map();
typeof m[Symbol.iterator]; // "function"
We can implement our own iterator object by filling the [Symbol.iterator] property with an iterator method.
const obj = { name: 'Faisal', age: 17 };
function myIterator() { // our iterator method implementation }
obj[Symbol.iterator] = myIterator;
We can also borrow an iterator method from another object (and indeed one of the benefits of this protocol is that iterators can be reused).
const obj = {
0: "a",
1: "b",
2: "c",
3: "d",
length: 4,
// hehehe
[Symbol.iterator]: Array.prototype[Symbol.iterator],
};
for (value of obj) console.log(value); // "a", "b", "c", "d"
The function we attach as an iterator method to our object cannot be just any function — it must satisfy another protocol called the iterator protocol.
The Iterator Protocol
The next protocol we will explore is the iterator protocol. The iterator protocol classifies objects that can define the iteration behavior of an object. For an object to become an iterator, this protocol requires the object to implement a next function that:
- Takes no arguments.
- Returns an object with the following properties:
value, which returns any value valid in JavaScript.done, which returns a Boolean indicating whether the iterator can produce the nextvalueor not. A returned object without adoneproperty is treated the same as returning{value, done: false}, meaning iteration can continue. Meanwhile, ifdoneistrue, the iterator will not return avalue.
Talk is confusing, show me the code:
// this function is an iterator method
function myIterator() {
let n = 0;
// this function returns an iterator object
return {
next: function () {
n++;
return { value: n, done: false };
},
};
}
const it = myIterator(); // it is an iterator object
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
Note that this iterator will never produce a done value of true, meaning iteration with this iterator will be endless — an infinite loop. Yes, that is allowed.
Now let’s try an iterator with different behavior on an object. Say we want our iterator to make all odd numbers even in the array we have by multiplying them by 2, while leaving already-even numbers alone.
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function evenizingIterator() {
let idx = 0;
return {
// using an arrow function so `this` is bound
// to the context of the array it is attached to
next: () => {
if (idx < this.length) {
const value = this[idx] % 2 === 0 ? this[idx] : this[idx] * 2;
idx = idx + 1;
return { value: value, done: false };
} else {
return { done: true };
}
},
};
}
arr[Symbol.iterator] = evenizingIterator;
for (v of arr) console.log(v);
// 0, 2, 2, 6, 4, 10, 6, 14, 8, 18, 10
So, is making an object iterable only useful for using for-loops? Of course not — there are many more JavaScript syntaxes that consume iterables.
// Array.from
const arr2 = Array.from(arr);
// spread, rest, and destructuring assignment
const arr3 = [...arr];
const [a, b, c] = [...arr];
const [d, e, f, ...g] = [...arr];
// other examples
Promise.all(iterable);
Promise.race(iterable);
So, What Are the Benefits of Using Iterables?
First, as we saw above, iterables allow us to use several native features of JavaScript. Beyond that:
- They separate data consumption logic from business logic. By separating the iterator, the iteration expression we need to write is free from logic and easier to read.
- They make it easier to reuse data consumption logic — remember that iterators can be reused.
- They allow consuming data from data sources of unknown or even infinite length via the
iterator.next()method.
That wraps up the overview of iterables and iterators — I hope it helps power up your iterations going forward. There is actually one more related topic, namely generators which act as a factory for iterators, but since this has already gotten quite long I’ll save it for a separate post. Stay tuned!
Further reading: