unModified()

Break stuff. Now.

Understanding JavaScript Iterators

And the subtle difference from arrays

March 21, 2021

nixie tubes with the number -9286 Photo by Bert from Pexels

For a long time, I never understood the purpose of iterators. I remember taking breaks at work where, instead of watching some random YouTube video, I'd be searching the internet for "practical uses of iterators in JavaScript". There are legit but often unsatisfactory results, citing very contrived use cases, or just showing another way to write something that could have been done with a simple loop.

([1, 2, 3, 4, 5, 6])
  .filter(v => v % 2 === 0)
  .forEach(v => console.log(v))

Here we have an array of numbers, extracted for its even numbers, and printed to the console. Notice that we have an array of numbers which are predefined and thus loaded onto memory. Now imagine if the array wasn't just 5 numbers. Ignore the filter and forEach at this point, what if it contained a million records of grocery purchase information? Loading that much data into memory all at once can potentially starve the system of memory and crash the app.

That's where iterators and iterables come in.

  • Iterators are objects that implement the iterator protocol (i.e. in typed language terms, an interface). Iterators are objects with a next() method that return an object containing value and done properties. These define the current iteration value and the iterator state, respectively.

  • Iterables are objects that define their iteration behavior by defining an iterator stored in a property whose key is Symbol.iterator. We can think of iterables as custom array-like objects where we define how they loop. JavaScript's very own strings, arrays, maps, and sets are all iterables.

The key piece of functionality that wooshed over my head all this time is that with iterables, you execute a piece of code to return the value of an item. Iterables are objects that have no idea about its items, and has to ask its iterator for values. And since an iterator is arbitrary code, we are in full control of what the value is, where the value comes from, when we load the value into memory, and how we fetch the value. This means we can:

  • Represent a dataset as an object without loading all the data into memory.
  • Fetch values as needed instead of loading everything into memory at once.
  • Avoiding loading unneeded data (e.g. if we break the loop midway).
  • Consuming the iterable separately from its declaration (lazy evaluation).
  • Create array-like objects without array prototype hacks (e.g. jQuery objects).
  • Change the values on the fly depending on the iterator state.
  • Fetch data from any source besides memory (e.g. filesystem, network, etc.).
  • Optimize storage and retrieval by using custom data structures and algorithms.
  • And so much more!

While I use the built-in iterables a lot on a daily basis, I've yet to understand the full power of iterables and what else it's capable of. In fact, I just learned that async iterators exist and for await...of is a thing. This means we can loop through iterables whose backing data source is asynchronous, like a network call, or another thread, or some throttled operation!

My mind is blown.