UP | HOME

Iterator and its applications

Note: MDN's description of Symbol.iterator sucks you can read about it in Eloquent JavaScript instead: https://eloquentjavascript.net/06_object.html#h_z2tOOXM8qO

Edit: Actually, you need to read the MDN's document about iteration protocol.


A for...of loop in JavaScript will create a loop that iterating over iterable objects. As described in iterable protocol, an object is iterable if it implements the @@iterator method, it's a method that return an object conforming the iterator protocol. You can use Symbol.iterator symbol to implement this method. Some built-in iterable objects are String, Array, TypedArray, Map, Set,…

const obj = ...;

for (let o of obj) { } // Failed because obj is not iterable

obj[Symbol.iterator] = () => { ... };

for (let o of obj) { } // Iterable now

For example, String type has @@iterator method implemented, so, we can iterate over it like this:

const str = "hello";

for (let c of str) {
  console.log(c);
}

// Output:
// h
// e
// l
// l
// o

So, what is iterator protocol and how does [Symbol.iterator] (or @@interator) method implemented? An object is called an iterator if it implements a next() method, this method takes no argument, and returning an object with at least two properties value and done:

{ value: ..., done: false }

The value, is obviously, the thing you will get in each iteration, and if the done value is set to true, we will stop iterating.

You can create an object that satisfies both iterable and iterator protocols in this way:

let myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this }
};

This syntax is short and cool, but for the sake of readability, in the following sections, we'll use a more verbal syntax, it's long but much more easier to understand and reusable.

One more thing, instead of iterating over an iterator with for...of loop, you can do it manually:

let iter;
while (iter = iterator.next(), !iter.done) {
   // do something with iter.value
}

Let's say, we have an array of strings, and we want to iterate through that array and get the length of each string element.

const data = ["hey", "hello", "world"];

First, we will define a new class named LengthIterator, which has a next() function that return the next string length every time it's being called.

class LengthIterator {
    constructor(array) {
        this.data = array;
        this.index = -1;
    }

    next() {
        this.index++;
        if (this.index < this.data.length) {
            return {
                value: this.data[this.index].length,
                done: false
            };
        }
        return { done: true };
    }
}

Then, we implement a [Symbol.iterator] method for the input array data, the implementation is simply return a new instance of LengthIterator class, this will make data iterable.

data[Symbol.iterator] = () => new LengthIterator(data);

for (let len of data) {
    console.log(len);
}

// Output:
// 3
// 5
// 5

Easy, heh?


Now, let's try solving another problem. Use iterator to extend the String type to give it an ability to break a string into a list of words.

const text = "The quick brown fox jump over a lazy dog";

Same as the previous example, this time, we'll implement a WordIterator class, in the constructor of this class, we split the input string into words, then use the next() function to returning each of them.

class WordIterator {
    constructor(data) {
        this.data = data.split(/\s/);
        this.index = -1;
    }

    next() {
        this.index++;
        if (this.index < this.data.length) {
            return {
                value: this.data[this.index],
                done: false
            }
        }
        return { done: true };
    }
}

By default, if you iterating through a string, you'll get each of its character, since we don't want to change that default behavior, we'll extend it by implementing a words() function, which will return a new copy of the string with WordIterator implemented as its iterator.

String.prototype.words = function() {
    const iterable = this;
    iterable[Symbol.iterator] = () => new WordIterator(this);
    return iterable;
};

From now, whenever we want to iterate through a list of words in a string, we can call words() function while iterating:

for (let word of text.words()) {
    console.log(word);
}

// Output:
// "The" "quick" "brown" "fox" "jump" "over" "a" "lazy" "dog"

Oh, and you know what? In this implementation, we ignored the fact that Array is already iterable. In real world, we can just implement the words() method like this:

String.prototype.words = function() {
    return this.split(/\s/);
}

Let's try with another application of iterator, this time, we'll make a simple clone version of Python's range() method. If you do not know, Python's range() produces a list of numbers that could be iterate over.

Our range() method will have two parameters, from and to, for each iterate of the next() function, we'll increase the from value until it reaches the to value.

const range = (from, to) => {
    return {
        [Symbol.iterator]: function() { return this; },
        next: function() {
            if (from < to) {
                from++;
                return { value: from - 1, done: false };
            }
            return { done: true };
        }
    }
};

Now, let's iterate:

for (const i of range(0, 100)) {
    console.log(i);
}

As of now, you can't do some asynchronous operation inside iterator's next() method, but soon, you'll able to do so with the help of Async Iterator, which will be a part of ES2018. With that coming, you can implement a more complicated iterators, like the file streaming iterator, which will help us read file byte by byte, reading the streaming body of a fetch() call,…

Date: 2019-01-09 Wed 00:00

Author: Huy Tran

Created: 2019-04-22 Mon 14:41