In JS Array and Strings are iterable. It means you can loop over in a for...of
loop. But Object is not iterable. Trying to iterate an object within a for...of
loop will give you an error.
var obj = { 'name' : "Santanu", 'location' : "Kolkata" }; for(var key of obj){ alert(key); }
The above code will give you an Error : Object is not iterable. And that's why there is a special for...in
construct only suited for Objects. But what if you want to make an object iterable. That's possible too.
Consider the following example -
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 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).
for..of
starts, it calls that method once (or errors if not found). The method must return an iterator – an object with the method next
.for..of
works only with that returned object.for..of
wants the next value, it calls next()
on that object.next()
must have the form {done: Boolean, value: any}
, where done==true
means that the iteration is finished, otherwise value must be the new value.Here’s the full implementation for range
:
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 this iterator, 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: an important separation of concerns:
range
itself does not have the next()
method.range[Symbol.iterator]()
, and it handles the whole 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.
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
.
Arrays and strings are most widely used built-in iterables. For a string, for..of loops over its characters:
for (let char of "test") { // triggers 4 times: once for each character alert( char ); // t, then e, then s, then t }
Normally, internals of iterables are hidden from the external code. There’s a for..of
loop, that works, that’s all it needs to know. But to understand things a little bit deeper let’s see how to create an iterator explicitly. We’ll iterate over a string the same way as for..of
, but with direct calls. This code gets a string iterator and calls it “manually”:
let str = "Hello"; // does the same as // for (let char of str) alert(char); let iterator = str[Symbol.iterator](); while (true) { let result = iterator.next(); if (result.done) break; alert(result.value); // outputs characters one by one }
That is rarely needed, but gives us more control over the process than for..of. For instance, we can split the iteration process: iterate a bit, then stop, do something else, and then resume later.
There are two official terms that look similar, but are very different. Please make sure you understand them well to avoid the confusion.
Symbol.iterator
method, as described above.length
, so they look like arrays.Naturally, these properties can combine. For instance, strings are both iterable (for..of
works on them) and array-like (they have numeric indexes and length
).
But an iterable may be not array-like. And vice versa an array-like may be not iterable. For example, the range
in the example above is iterable, but not array-like, because it does not have indexed properties and length
.
And here’s the object that is array-like, but not iterable:
let arrayLike = { // has indexes and length => array-like 0: "Hello", 1: "World", length: 2 }; // Error (no Symbol.iterator) for (let item of arrayLike) {}
What do they have in common? Both iterables and array-likes are usually not arrays, they don’t have push
, pop
etc. That’s rather inconvenient if we have such an object and want to work with it as with an array.
There’s a universal method Array.from
that brings them together. It takes an iterable or array-like value and makes a “real” Array from it. Then we can call array methods on it.
let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // World (method works)
The same happens for an iterable object -
// assuming that range is taken from the example above let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (array toString conversion works)
The full syntax for Array.from
allows to provide an optional “mapping” function:
Array.from(obj[, mapFn, thisArg])
The second argument mapFn
should be the function to apply to each element before adding to the array, and thisArg
allows to set this for it. For example -
// assuming that range is taken from the example above // square each number let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25
Here we use Array.from
to turn a string into an array of characters:
let str = '𝒳😂'; // splits str into array of characters let chars = Array.from(str); // Same as str.split(""); alert(chars[0]); // 𝒳 alert(chars[1]); // 😂 alert(chars.length); // 2