- ES6 Symbols
- Javascript Generators
- ES6’s Many for Loops and Iterable Objects
- Async Generators and Async Iteration in Node.js
Today we’ve got another remedial-for-me dive into modern javascript practices. Specifically, we’ll be looking at the for ... of
loop.
The Old Ways
In the very old days there were two ways to loop over things in javascript.
First, there was your classic for i
loop. This let you loop over arrays or over any object that was indexable and had a length.
for(i=0;i<things.length;i++) {
var thing = things[i]
/* ... */
}
Second, there was the for ... in
loop for looping over the key/values of an object
for(key in things) {
if(!thing.hasOwnProperty(key)) { continue; }
var thing = things[key]
/* ... */
}
The for ... in
loop was often given side-eye because it loops over every enumerable property of an object. This includes properties from parent objects in the prototype chain, as well as any property assigned as a method. In other words — it loops over things some people (i.e. you — or at least me) might not expect. Using for ... in
usually means a lot of guard clauses in your loop block to avoid properties you don’t want.
Early javascript worked around this via libraries. Many javascript libraries (Prototype.js, jQuery, lodash, etc.) have something like an each
or foreach
utility method/function that let you loop over objects and arrays without needing a for i
loop or a for ... in
loop.
The for ... of
loop is ES6 trying to solve some of these problems without the need for a third party library.
For … of
A for ... of
loop
for(const thing of things) {
/* ... */
}
will loop over an iterable object.
An iterable object is an object that has an @@iterator method defined, and this @@iterator method either returns an object that implements the iterator protocol or the method is a generator function.
That’s a lot to take in all at once
- An iterable object
- An @@iterator method (what does
@@
mean?) - An iterator protocol (what’s a protocol mean here?)
- Wait, so iterable and iterator mean different things?
- Also, generator functions? Ugh.
We’ll take each of these step by step.
Built in Iterables
First off — a few of javascript’s built in objects are naturally iterable — array objects are the first that come to mind. Use an array in a for ... of
loop like this
const foo = [
'apples','oranges','pears'
]
for(const thing of foo) {
console.log(thing)
}
and each item of the array will be output.
apples
oranges
pears
There’s also the entries
method of an array. This method returns an iterable object. This iterable object returns both the keys and values for each trip through the loop. A program like this
const foo = [
'apples','oranges','pears'
]
for(const thing of foo.entries()) {
console.log(thing)
}
will output the following
[ 0, 'apples' ]
[ 1, 'oranges' ]
[ 2, 'pears' ]
This entries
method is more useful when you’re using the alternate syntax below
const foo = [
'apples','oranges','pears'
]
for(const [key, value] of foo.entries()) {
console.log(key,':',value)
}
Here, we’re declaring two variables in our for loop — one for the first item of the returned array (the key/index of the value) and another for the second item (the actual value at that index).
A plain javascript object is not iterable. If you try this
// will not work
const foo = {
'apples':'oranges',
'pears':'prunes'
}
for(const [key, value] of foo) {
console.log(key,':',value)
}
You’ll get an error
$ node test.js
/path/to/test.js:6
for(const [key, value] of foo) {
TypeError: foo is not iterable
However — the static entries
method of the global Object
object accepts a plain object as an argument and returns an object that is iterable. In other words, a program like this
const foo = {
'apples':'oranges',
'pears':'prunes'
}
for(const [key, value] of Object.entries(foo)) {
console.log(key,':',value)
}
will produce the output you might expect
$ node test.js
apples : oranges
pears : prunes
Creating your Own Iterable
If you want to create your own iterable object, you’re in for a more complicated time. You’ll recall we said
An iterable object is an object that has an @@iterator method defined, and this @@iterator method either returns an object that implements the iterator protocol or the method is a generator function.
This is similar to how you might implement the Iterator
interface in PHP — except that javascript has a strange history (and a strange present) with traditional class
based object oriented programming, and doesn’t have interfaces.
The easiest way I’ve found to understand all this is to just walk through creating the iterable object step by step. First, we’ll need an object that implements an @@iterator method. The @@
notation is a little misleading — what we really needs to do is define a method using the predefined Symbol.iterator
symbol. If you’re not familiar with using symbols to define methods we’ve written about that previously over here.
If we define an object with an iterator method and try to loop through it
const foo = {
[Symbol.iterator]: function() {
}
}
for(const [key, value] of foo) {
console.log(key, value)
}
we get a new error.
for(const [key, value] of foo) {
^
TypeError: Result of the Symbol.iterator method is not an object
This is javascript telling us it tried calling our Symbol.iterator
method, but the results of this call were not an object.
In order to get rid of this error, we need our iterator method to return an object that implements the iterator protocol. All that means is the iterator method needs to return an object that has a next
key, and where the next
key is a function.
const foo = {
[Symbol.iterator]: function() {
return {
next: function() {
}
}
}
}
for(const [key, value] of foo) {
console.log(key, value)
}
If we run the above code, we get a new error.
for(const [key, value] of foo) {
^
TypeError: Iterator result undefined is not an object
This time javascript is telling us it tried calling our Symbol.iterator
method, and while the object was an object and did implement the next
method, that the return value of next
wasn’t the object javascript expected.
The next
function needs to return an object with a very specific format — with a value
and done
key.
next: function() {
//...
return {
done: false,
value: 'next value'
}
}
The done
key is optional. If true — it indicates the iterator is done iterating — that it has reached the end of its iteration.
If done
is false
or not present, then the value
key is required. The value
key is the value that should be returned for this trip through the loop.
So, put into code, here’s a different program with a simple iterator that returns the first ten even numbers.
class First20Evens {
constructor() {
this.currentValue = 0
}
[Symbol.iterator]() {
return {
next: (function() {
this.currentValue+=2
if(this.currentValue > 20) {
return {done:true}
}
return {
value:this.currentValue
}
}).bind(this)
}
}
}
const foo = new First20Evens;
for(const value of foo) {
console.log(value)
}
Generators
Manually creating an object that implements the iterator protocol isn’t your only option. Generator objects (returned by generator functions) also implement the iterator protocol. The above example built with a generator would look something like this
class First20Evens {
constructor() {
this.currentValue = 0
}
[Symbol.iterator]() {
return function*() {
for(let i=1;i<=10;i++) {
if(i % 2 === 0) {
yield i
}
}
}()
}
}
const foo = new First20Evens;
for(const item of foo) {
console.log(item)
}
We’re not going to get too into generators in this article — if you need a primer we’ve talked about them recently over here. The important take away for today is that we can have our Symbol.iterator
method return a generator object, and the generator object will “just work” in the for ... of
loop. By “just work” we mean the loop will continue to call next
on the generator until the generator stops yield
ing values.
$ node sample-program.js
2
4
6
8
10
Wrap Up
With for ... of
loops covered, next time we’ll finish our trip down javascript’s looping lane with a look at asynchronous iterators.