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 whentrue
, andfalse
means there may be more valuesvalue
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.
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!
Comments