ponyfoo.com

ES6 Iterators in Depth

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

This is yet another edition of ES6 in Depth. First time here? Welcome! So far we covered destructuring, template literals, arrow functions, the spread operator and rest parameters, improvements coming to object literals, the new classes sugar on top of prototypes, and an article on let, const, and the “Temporal Dead Zone”. The soup of the day is: Iterators.

Like I did in previous articles on the series, I would love to point out that you should probably set up Babel and follow along the examples with either a REPL or the babel-node CLI and a file. That’ll make it so much easier for you to internalize the concepts discussed in the series. If you aren’t the “install things on my computer” kind of human, you might prefer to hop on CodePen and then click on the gear icon for JavaScript – they have a Babel preprocessor which makes trying out ES6 a breeze. Another alternative that’s also quite useful is to use Babel’s online REPL – it’ll show you compiled ES5 code to the right of your ES6 code for quick comparison.

Before getting into it, let me shamelessly ask for your support if you’re enjoying my ES6 in Depth series. Your contributions will go towards helping me keep up with the schedule, server bills, keeping me fed, and maintaining Pony Foo as a veritable source of JavaScript goodies.

Thanks for listening to that, and without further ado… shall we?

Iterator Protocol and Iterable Protocol

There’s a lot of new, intertwined terminology here. Please bear with me as I get some of these explanations out of the way!

JavaScript gets two new protocols in ES6, Iterators and Iterables. In plain terms, you can think of protocols as conventions. As long as you follow a determined convention in the language, you get a side-effect. The iterable protocol allows you to define the behavior when JavaScript objects are being iterated. Under the hood, deep in the world of JavaScript interpreters and language specification keyboard-smashers, we have the @@iterator method. This method underlies the iterable protocol and, in the real world, you can assign to it using something called “the “well-known” Symbol.iterator Symbol”.

We’ll get back to what Symbols are later in the series. Before losing focus, you should know that the @@iterator method is called once, whenever an object needs to be iterated. For example, at the beginning of a for..of loop (which we’ll also get back to in a few minutes), the @@iterator will be asked for an iterator. The returned iterator will be used to obtain values out of the object.

Let’s use the snippet of code found below as a crutch to understand the concepts behind iteration. The first thing you’ll notice is that I’m making my object an iterable by assigning to it’s mystical @@iterator property through the Symbol.iterator property. I can’t use the symbol as a property name directly. Instead, I have to wrap in square brackets, meaning it’s a computed property name that evaluates to the Symbol.iterator expression – as you might recall from the article on object literals. The object returned by the method assigned to the [Symbol.iterator] property must adhere to the iterator protocol. The iterator protocol defines how to get values out of an object, and we must return an @@iterator that adheres to iterator protocol. The protocol indicates we must have an object with a next method. The next method takes no arguments and it should return an object with these two properties.

  • done signals that the sequence has ended when true, and false means there may be more values
  • value is the current item in the sequence

In my example, the iterator method returns an object that has a finite list of items and which emits those items until there aren’t any more left. The code below is an iterable object in ES6.

var foo = {
  [Symbol.iterator]: () => ({
    items: ['p', 'o', 'n', 'y', 'f', 'o', 'o'],
    next: function next () {
      return {
        done: this.items.length === 0,
        value: this.items.shift()
      }
    }
  })
}

To actually iterate over the object, we could use for..of. How would that look like? See below. The for..of iteration method is also new in ES6, and it settles the everlasting war against looping over JavaScript collections and randomly finding things that didn’t belong in the result-set you were expecting.

for (let pony of foo) {
  console.log(pony)
  // <- 'p'
  // <- 'o'
  // <- 'n'
  // <- 'y'
  // <- 'f'
  // <- 'o'
  // <- 'o'
}

You can use for..of to iterate over any object that adheres to the iterable protocol. In ES6, that includes arrays, any objects with an user-defined [Symbol.iterator] method, generators, DOM node collections from .querySelectorAll and friends, etc. If you just want to “cast” any iterable into an array, a couple of terse alternatives would be using the spread operator and Array.from.

console.log([...foo])
// <- ['p', 'o', 'n', 'y', 'f', 'o', 'o']
console.log(Array.from(foo))
// <- ['p', 'o', 'n', 'y', 'f', 'o', 'o']

To recap, our foo object adheres to the iterable protocol by assigning a method to [Symbol.iterator] – anywhere in the prototype chain for foo would work. This means that the object is iterable: it can be iterated. Said method returns an object that adheres to the iterator protocol. The iterator method is called once whenever we want to start iterating over the object, and the returned iterator is used to pull values out of foo. To iterate over iterables, we can use for..of, the spread operator, or Array.from.

What Does This All Mean?

In essence, the selling point about iteration protocols, for..of, Array.from, and the spread operator is that they provide expressive ways to effortlessly iterate over collections and array-likes (such as arguments). Having the ability to define how any object may be iterated is huge, because it enables any libraries like lo-dash to converge under a protocol the language natively understands – iterables. This is huge.

Just to give you another example, remember how I always complain about jQuery wrapper objects not being true arrays, or how document.querySelectorAll doesn’t return a true array either? If jQuery implemented the iterator protocol on their collection’s prototype, then you could do something like below.

for (let item of $('li')) {
  console.log(item)
  // <- the <li> wrapped in a jQuery object
}

Why wrapped? Because it’s more expressive. You could easily iterate as deep as you need to.

for (let list of $('ul')) {
  for (let item of list.find('li')) {
    console.log(item)
    // <- the <li> wrapped in a jQuery object
  }
}

This brings me to an important aspect of iterables and iterators.

Lazy in Nature

Iterators are lazy in nature. This is fancy-speak for saying that the sequence is accessed one item at a time. It can even be an infinite sequence – a legitimate scenario with many use cases. Given that iterators are lazy, having jQuery wrap every result in the sequence with their wrapper object wouldn’t have a big upfront cost. Instead, a wrapper is created each time a value is pulled from the iterator.

How would an infinite iterator look? The example below shows an iterator with a 1..Infinity range. Note how it will never yields done: true, signaling that the sequence is over. Attempting to cast the iterable foo object into an array using either Array.from(foo) or [...foo] would crash our program, since the sequence never ends. We must be very careful with these types of sequences as they can crash and burn our Node process, or the human’s browser tab.

var foo = {
  [Symbol.iterator]: () => {
    var i = 0
    return { next: () => ({ value: ++i }) }
  }
}

The correct way of working with such an iterator is with an escape condition that prevents the loop from going infinite. The example below loops over our infinite sequence using for..of, but it breaks the loop as soon as the value goes over 10.

for (let pony of foo) {
  if (pony > 10) {
    break
  }
  console.log(pony)
}

The iterator doesn’t really know that the sequence is infinite. In that regard, this is similar to the halting problem – there is no way of knowing whether the sequence is infinite or not in code.

The halting problem depicted by XKCD
The halting problem depicted by XKCD

We usually have a good idea about whether a sequence is finite or infinite, since we construct those sequences. Whenever we have an infinite sequence it’s up to us to add an escape condition that ensures our program won’t crash in an attempt to loop over every single value in the sequence.

Come back tomorrow for a discussion about generators!

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments