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 | |
}) | |
} | |
} | |
}) |
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] |
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 | |
}) | |
}) | |
} | |
} |
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] |
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]) | |
}) | |
} | |
} |
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] | |
} |
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)