Categories


Archives


Recent Posts


Categories


ES6’s Many for Loops and Iterable Objects

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This entry is part 3 of 4 in the series Four Steps to Async Iterators. Earlier posts include ES6 Symbols, and Javascript Generators. Later posts include 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

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 yielding 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.

Series Navigation<< Javascript GeneratorsAsync Generators and Async Iteration in Node.js >>

Copyright © Alan Storm 1975 – 2020 All Rights Reserved

Originally Posted: 28th July 2020