JS Tip of the Day: The Iterator Protocol

The Iterator Protocol
Version: ES2015
Level: Advanced

Iterator objects are objects used to iterate through a set of values. The iterator protocol defines the interface or expectations for iterator objects. The iterator protocol defines iterator objects as having the following properties:

  • next(): A function that returns an iterator result object describing the next value during iteration.
  • return(): [Optional] A function used for telling the iterator that the caller wants to close the iterator and no longer accept new values. The return value is an iterator result object.
  • throw(): [Optional] A function used for telling the iterator that the caller has encountered an error. The return value is an iterator result object.

Iterator result objects have the following properties:

  • value: [Optional] The value for the current step in the iteration of a set of values. If not provided, undefined is assumed.
  • done: [Optional] A flag indicating whether or not the iterator has exhausted all of its iterable values. If out of values, this value will be true. If not provided, false is assumed.

Generator objects returned from generator functions are iterator objects. They will implement each of the methods of the iterator protocol which will each return iterator result objects when called.

function * abc () {
    yield 'a';
    yield 'b';
    yield 'c';
}

let abcIterator = abc();
console.log(typeof abcIterator.next); // function
console.log(typeof abcIterator.return); // function
console.log(typeof abcIterator.throw); // function

console.log(abcIterator.next()); // {value: 'a', done: false}
console.log(abcIterator.next()); // {value: 'b', done: false}
console.log(abcIterator.next()); // {value: 'c', done: false}
console.log(abcIterator.next()); // {value: undefined, done: true}

You can make your own iterator objects from scratch as long as they conform to the iterator protocol. The next() function is the only required function for iterators and all it needs to do is return an object. With that, the simplest possible iterator would be the following (it would iterate through an infinite set of undefined values):

let iterator = { // iterator object
    next () {
        return {}; // iterator result
    }
};

console.log(iterator.next()); // {}
console.log(iterator.next()); // {}
console.log(iterator.next()); // {}
// ...

A more useful iterator object would iterate through a set of (more useful) values and indicate when iteration is complete by making use of the properties in the iterator result object.

let fruitIterator = {
    fruits: ['apple', 'orange', 'banana'],
    next () {
        let hasFruit = this.fruits.length > 0;
        return {
            value: this.fruits.shift(), // undefined if fruits empty
            done: !hasFruit
        };
    }
};

console.log(fruitIterator.next()); // {value: 'apple', done: false}
console.log(fruitIterator.next()); // {value: 'orange', done: false}
console.log(fruitIterator.next()); // {value: 'banana', done: false}
console.log(fruitIterator.next()); // {value: undefined, done: true}

This iterator could be expanded to implement a return() method as well.

let fruitIterator = {
    fruits: ['apple', 'orange', 'banana'],
    next () {
        let hasFruit = this.fruits.length > 0;
        return {
            value: this.fruits.shift(),
            done: !hasFruit
        };
    },
    return () {
        this.fruits.length = 0; // clear items
        return this.next();
    }
};

console.log(fruitIterator.next()); // {value: 'apple', done: false}
console.log(fruitIterator.return()); // {value: undefined, done: true}
console.log(fruitIterator.next()); // {value: undefined, done: true}

It’s uncommon that you’d iterate through an iterator manually by calling next() yourself. Most of the time iterators are managed internally when interacting with iterable objects. Just as we’ve done in a previous tip with generators, we can also create an iterable object by having its Symbol.iterator method be a normal function that returns an iterator object.

let fruitBasket = {
    contents: ['apple', 'orange', 'banana'],
    [Symbol.iterator] () { // non-generator
        let index = 0;
        return { // returning an iterator object
            next: () => {
                let hasFruit = index < this.contents.length;
                return {
                    value: this.contents[index++],
                    done: !hasFruit
                };
            }
        };
    }
};

As an iterable object, you can use a standard for...of loop to go through its values. The iterations within the loop are determined by the iterator object returned by the Symbol.iterator method.

for (let fruit of fruitBasket) {
    console.log(fruit);
}
/* logs:
apple
orange
banana
*/

And if the iterator implemented a return(), that would get called if for any reason you exited the for loop prematurely.

let fruitBasket = {
    contents: ['apple', 'orange', 'banana'],
    [Symbol.iterator] () { // non-generator
        let index = 0;
        return { // returning an iterator object
            next: () => {
                let hasFruit = index < this.contents.length;
                return {
                    value: this.contents[index++],
                    done: !hasFruit
                };
            },
            return () {
                console.log('Iterator return');
                return { done: true };
            }
        };
    }
};
for (let fruit of fruitBasket) {
    console.log(fruit);
    break;
}
/* logs:
apple
Iterator return
*/

Note that in this iterable case the next() of the iterator object is defined as an arrow function. This allows it to keep the context of the Symbol.iterator method since the iterable data (contents) is in the iterable object rather than the iterator itself. The method of iteration also changed, accessing values by index rather than shifting them off because the iterator of an iterable should be non-destructive keeping the original object data intact. Iterators themselves, on the other hand, are one-time use objects that run to completion once and that’s it.

More info: