ECMAScript.next: for-of, iterators, generators

[2012-06-11] esnext, dev, javascript
(Ad, please don’t block)
[2015-02-26] New version of this blog post: “Iterables and iterators in ECMAScript 6

ECMAScript.next’s for-of loop will provide a new and quirk-free way of iterating over all kinds of data. This blog post explains how it works and how you can try out a prototype implementation in Firefox 13 and later.

Iterating over data

The for-of loop is convenient for iterating over arrays and objects.

Iterating over arrays

The standard for-in loop has several quirks: It iterates over all enumerable properties of an object, including inherited ones. That means it is ill-suited for iterating over arrays, because it does not iterate over array elements. You cannot even use it to iterate over array indices, because non-index property names are always included. As an example, take the following array.
    let arr = [ "blue", "green" ];
    arr.notAnIndex = 123;
    Array.prototype.protoProp = 456;
If you iterate over it with for-in, you get:
    > for(k in arr) console.log(k)
    0
    1
    notAnIndex
    protoProp
Hence, for ECMAScript 5, it is better to use Array.prototype.forEach [1]. ECMAScript.next will give us the for-of loop that also works as expected:
    > for(k of arr) console.log(k)
    blue
    green

Iterating over objects

Objects are not iterable by default. The rationale for this decision is that people will mainly iterate over collections (instances of Array, ECMAScript.next’s Map, etc.). If you are currently iterating over an object, you are usually (ab)using it as a collection. But collections are also objects, so the different operations “iterate over a collection” and “iterate over an object” should not clash. Which is why you have to apply a function to an object if you want to iterate over its properties. ECMAScript next will have a module @iter with appropriate tool functions. The neat thing is that these functions don’t return arrays, they return an iterator (see below). That means that they compute elements on demand, whenever an iteration mechanism requests the next one. Current functions that inspect objects, such as Object.keys() always compute the complete result right away, usually as an array. Consult the iterator proposal for details on @iter. The functions for iterating over objects are:
  • keys(obj): iterates over the names of all own (non-inherited) properties of obj.
  • values(obj): iterates over the values of all own properties of obj.
  • items(obj): iterates over all own properties, as [name, value] pairs.
  • allKeys(obj): iterates over the names of all properties of obj (including inherited ones).
  • allValues(obj): iterates over the values of all properties of obj.
  • allItems(obj): iterates over all properties, as [name, value] pairs.
Thus, the following code prints out all own properties of an object:
    import items from "@iter";
    let obj = { first: "Jane", last: "Doe" };
    for (let [k,v] of items(obj)) {
        console.log(k+" = "+v);
    }
Output:
    first = Jane
    last = Doe
Firefox does not yet have the module @iter (or modules in general), but you can use the function Iterator() as a work-around:
    let obj = { first: "Jane", last: "Doe" };
    for (let [k,v] of Iterator(obj)) {
        console.log(k+" = "+v);
    }

Iterators

ECMCAScript.next iterators allow one to implement a custom iteration strategy for a data structure. To achieve the same under ECMAScript 5, one usually creates a new array and iterates over it via Array.prototype.forEach(). For example, Object.keys() can be seen as a custom iteration strategy for objects: It iterates over the enumerable own property names. However, each time it is invoked, it creates an array with the names. An iterator makes this simpler. The following is an example of an object that comes with custom iteration.
    let obj = {
        data: [ "hello", "world" ],
        
        // Custom iteration:
        __iterator__: function () {
            let index = 0;
            let that = this;
            // Return iterator object
            return {
                next: function () {
                    if (index < that.data.length) {
                        return that.data[index++];
                    } else {
                        throw StopIteration;
                    }
                }
            }
        }
    }
The special method __iterator__ returns an iterator object. Such an object has a method next() that either returns the next element in the current iteration sequence or throws StopIteration if there are no more elements. Firefox 13’s for-of does not yet support the __iterator__ method. Once it does, one will be able to iterate over obj like this:
    > for (x of obj) console.log(x);
    hello
    world
Note that the final version of for-of will probably use a special mechanism to specify the name of the iterator method. That is, it won’t have the name __iterator__.

Generators

Among other things, generators help with implementing iterators. The above iterator could be implemented as follows via a generator:
    let obj = {
        data: [ "hello", "world" ],
        
        // function* means: generator
        __iterator__: function* generator() {
            for(let index=0; index < this.data.length; index++) {
                yield this.data[index];
            }
        }
    }
Let’s use a generator to iterate over the [property name, property value] entries of an object.
    function* items(obj) {  // Firefox 13: function items...
        for(let key in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
                yield [ key, obj[key] ];
            }
        }
    }
The above code works in Firefox 13, but you have to omit the * after function. You use items() as follows:
    > let obj = { first: "Jane", last: "Doe" };
    > for (x of items(obj)) console.log(x);
    ["first", "Jane"]
    ["last", "Doe"]
You can also destructure the array if you are interested in keys and values:
    > for ([k,v] of items(obj)) console.log(k+" = "+v);
    first = Jane
    last = Doe

Related reading

  1. Iterating over arrays and objects in JavaScript
  2. Bug 699565 – Implement Harmony for-of loops
  3. for...of - MDN