thank u, [Symbol.iterator].next
Thank u, [Symbol.iterator].next
Thank u, [Symbol.iterator].next
I’m so f****in’ grateful for this spec
The most obvious use case is in creating new, iterable data structures that are not provided by the language, like a Linked List. However,
Symbol.iterator also lets developers redefine navigation patterns without cloning the object or modifying its order in-place.
These patterns can then be written once in a function, class, or module and used anywhere. That means you can loop through an
Array forwards, backwards, randomly, or infinitely in a modular fashion.
Symbol.iterator protocol is very simple. It includes:
- Iterable: an object with a function whose key is
- Iterator: the above function, used to obtain the values to be iterated
In terms of code:
If you want more details, there is a great guide on Demystifying ES6 Iterables & Iterators. Otherwise, let’s look at how to use this in practice.
The two most common ways to iterate an array in reverse are using indices (from
array.length — 1 to
0), or with
But both approaches raise concerns. With indices, it can be easy to forget the
-1, leading to the infamous off-by-one error. Using
Array.reverse modifies the array in place, which may not always be the intention.
There are entire articles dedicated to this topic, but since we are talking about
Symbol.iterator, let me suggest one more way:
Rather than iterating backwards on a given array, we have defined the “going backwards” iteration behavior for any array, all without modifying the order or creating a clone of the original array.
Here is how
reverse would work in practice:
Another important aspect of
Symbol.iterator is that we do not need an Array-like object to iterate. We can write a function like Python’s
range and use it to iterate elegantly over a sequence of numbers.
Note: this implementation of
range is very simple. It has not been tested and has clear issues with infinite ranges and negative steps.
We can use
range the same way we did
Note: I wrapped the number literal in parentheses to avoid a SyntaxError, but there are other options.
As we will see, with
Symbol.iterator the possibilities are literally endless!
Thank U, Next
A music player is a great example of iteration behavior. Most of us do not listen to songs in order. Instead we control playback with shuffle and repeat.
As a programmer, this means building a way to play songs infinitely (repeat), randomly (shuffle), or both. Here is an example that does just that:
The logic underlying these playback options is implemented using
Symbol.iterator. Let me quickly break down how each class works.
Playing songs on repeat is the simplest task. With
InfiniteArray, when we reach the end of the array we just go back to the first element, ad infinitum.
InfiniteArray might be the simplest, but it is also the most dangerous! You can easily freeze a tab by calling
Thankfully this is just an example, and Fisher-Yates is by far the simplest shuffling algorithm so we will go with that.
RandomArray does not modify the Array in place, we can use it to create a shuffled (randomly-sorted) copy of the original Array.
Note: The actual order will vary each time the spread operator is called.
Repeat & Shuffle
Some users will enable both shuffle and repeat simultaneously.
RandomInfiniteArray does just that, iterating both randomly and infinitely.
Caution: same warning as
InfiniteArray, be careful looping infinitely!
Each of these classes can be used anywhere an Array is expected. In this example,
updateIterator creates a shallow copy of the original album list with a type that depends on which playback options are selected.
In a larger application,
Symbol.iterator can cleanly encapsulate iteration behavior in classes or functions, decreasing code repetition and error risk.
It is time to break free from the standard for loop! Thus far, I have shown how
Symbol.iterator offers significant advantages by encapsulating iteration patterns for a single array. What about multiple arrays?
Looping over two arrays pairwise in Vanilla JS quickly litters code with
[i], but because this is a common task libraries like Lodash come with a
_.zip function that combines multiple sequential collections into one collection.
Using variadic arguments and
Symbol.iterator, we can easily write our own version of
zip that iterates over multiple arrays but without needing to clone the entirety of each array.
Now we have a modular and readable way to iterate over multiple arrays at the same time! Extending the previous example to include a third dimension:
Of course accurate, generalizable benchmarks are difficult and it is important to measure in performance-critical settings. But generally speaking,
Symbol.iterator uses less memory and should be at least as fast, if not faster, than other approaches.
Yet another use of iterators, is to stop iterating!
This is similar to
_.takeWhile, although again it does not require cloning.
One Last Time
Let me end with one more example. In the world of Snapchat and ephemeral messaging, what if we could create an Array using invisible ink?
Ok, not literally but you get the idea.
That is what I call
VanishingArray, an Array that removes elements as you loop through them. Iterate once, and only once.
Before you move on too fast, it should be noted that
Symbol.iterator has its downsides and should probably not be the default approach to every problem.
For one, it is often more complicated and verbose than simply chaining a combination of
Array.map. There are also other approaches with equal or better performance characteristics, like data streams and libraries like Sequency.
Symbol.iterator is versatile and efficient, and I have only presented a small subset of its utility. MDN has examples using generators with
Symbol.asyncIterator (that’s right, asynchronous iterators!). If you are curious about the
Symbol API more generally, Keith Cirkel has a great article on Metaprogramming in ES6.