DEV Community

Thomas Barrasso
Thomas Barrasso

Posted on

Symbol.iterator for modular iteration without cloning or modifying in-place

Symbol.iterator is the protocol that makes native objects like Array, Set, and Map iterable. It provides a hook into language features like the spread operator and for…of loops.

With Symbol.iterator, developers can redefine navigation patterns without cloning or modifying an object in-place. That means iterating forwards, backwards, randomly, infinitely, pairwise, or just about any way you can imagine.

The protocol specification is very simple:

  • Iterable: the object with a function whose key is Symbol.iterator
  • Iterator: the iterable function, used to obtain the values to be iterated
const iterable = {
[Symbol.iterator](): iterator
}
const iterator = {
next: () => ({
value: any,
done: boolean
})
}

These two properties tell the Javascript runtime what the next value is and when iteration is done.

Consider this example for iterating in reverse:

const reverse = arr => ({
[Symbol.iterator]() {
let i = arr.length;
return {
next: () => ({
value: arr[--i],
done: i < 0
})
}
}
})
view raw reverse.js hosted with ❤ by GitHub

Instead of iterating backwards using indices or Array.reverse, we have defined the “going backwards” behavior for any array. We have done so while preserving the order and without creating a clone of the original array.

reverse can now be used in for...of loops or with the spread operator:

let nums = [1, 2, 3, 4, 5]
for (let num of reverse(nums)) {
// ...
}
console.log(nums) // [1, 2, 3, 4, 5]
console.log([...reverse(nums)]) // [5, 4, 3, 2, 1]
view raw reverse_ex.js hosted with ❤ by GitHub

Symbol.iterator can also be used to recreate Python's range function.

const range = (start = 0, stop, step = 1) => {
if (stop === undefined) {
[start, stop] = [0, start]
}
start -= step
return {
[Symbol.iterator]: () => ({
next: () => ({
value: start += step,
done: start >= stop
})
})
}
}
view raw range2.js hosted with ❤ by GitHub

Now we can elegantly iterate over a range of numbers without needing to create an intermediate array.

for (let i of range(1, 10)) {
// ...
}
console.log([...range(1, 10)]) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log([...range(10)]) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log([...range(0, 10, 2)]) // [0, 2, 4, 6, 8]
view raw RangeExample.js hosted with ❤ by GitHub

It even seems that performance is comparable to a standard for loop.

We do not even need to limit ourselves to a single collection. Like many libraries, we can write a zip function that combines multiple sequential collections of equal length, without needing to clone them in their entirety.

function zip(...arrs) {
let i = -1;
return {
[Symbol.iterator]() {
return this;
},
next: () => ({
done: ++i === arrs[0].length,
value: arrs.map(arr => arr[i])
})
}
}
view raw zip2.js hosted with ❤ by GitHub

This allows us to seamlessly iterate pairwise (or triplet-wise):

let xs = [1, 5, 10]
let ys = [10, 50, 100]
let zs = [0.01, 0.5, 0.1]
for (let [x, y, z] of zip(xs, ys, zs)) {
[x, y, z]
}
view raw ZipExample.js hosted with ❤ by GitHub

In this case, Symbol.iterator significantly outperforms an cloning implementation using Array.map.

There are many uses for Symbol.iterator, and while it is more verbose than Array.map, it is more versatile and potentially more efficient.

Top comments (0)