Saturday, 20 May 2017

Generators and Iterators in ECMAScript 6

This blog post is intended to ease the understanding of iterators and generators in ES6. 


In ES6 the for..of operator allows developers to directly iterate over values unlike the for..in operator in vanilla JavaScript which iterates over index. Some object types in ES6 have built-in iterators by default like the StringArrayTypedArrayMap and Set. But the Object literal ({"key":"value"}) cannot be iterated by the for..of operator because it does not implement the @@iterator method.



Fig 1.0 ES6 Iterable Objects

These objects can be better understood by investigating the Iterable Protocol and the Iterator Protocol.

Iterable Protocol:


The iterable protocol allows ES6 objects to implement their own iteration functionality. In order to have iteration functionality ES6 objects need to implement the @@iterator method.
The @@iterator is actually the key to the actual function. This key should be present on the prototype chain of the object that requires iteration functionality.


Fig 2.0 Overview of for..of with iterable objects


The @@iterator method is accessible via Symbol.iterator key. When an Object is iterated using for..of operator its @@iterator method from the prototype chain returns a function that provides the iterator to access Object's values.

The code below shows an example of built-in iterable which is  the String object.


var someString = 'Hello World';
console.log(typeof someString[Symbol.iterator]); // "function"

for(let val of someString){
  console.log(val);
}

//"H"
//"e"
//"l"
//"l"
//"o" //" " //"W" //"o" //"r" //"l" //"d"

Iterator Protocol:

Using the iterator protocol developers can define a specific way to produce values for any object. It can be done by implementing next() method. This next() method should return an object literal containing 'done' and 'value' properties.

In the code below a custom array iterator is implemented using the next() method. When the fruits array is passed the values can be obtained by calling next().value and when the iteration is done it returns next().done as true.


function fruitsIterator(array) {
    var nextIndex = 0;
    
    return {
       next: function() {
           return nextIndex < array.length ?
               {value: 'Fruit: '+array[nextIndex++], done: false} :
               {done: true};
       }
    };
}

var frIt = fruitsIterator(['Apple', 'Orange', 'Kiwi']);

console.log(frIt.next().value); // 'Fruit: Apple'
console.log(frIt.next().value); // 'Fruit: Orange'
console.log(frIt.next().value); // 'Fruit: Kiwi'
console.log(frIt.next().done);  // true

Generator:

In simple terms the generator object can behave like an iterable and an iterator. It is also able to maintain state of its own. The function* deceleration returns a generator object.




Fig 3.0 Overview of generator

The code below shows different use cases of generators. Notice that 'console.log(range(0, 10, 2));' actually returns the generator object which can be used to access iterable or iteration behavior.


//Use as object
let fibonacci = {
    *[Symbol.iterator]() {
        let pre = 0, cur = 1
        for (;;) {
            [ pre, cur ] = [ cur, pre + cur ]
            yield cur
        }
    }
}

for (let n of fibonacci) {
    if (n > 1000)
        break;
    console.log(n);//1, 2, 3, 5
}

//Use as function
function* range (start, end, step) {
    while (start < end) {
        yield start
        start += step
    }
}

console.log(range(0, 10, 2));
//GeneratorFunctionPrototype {
//  "_invoke": [Function invoke]
//}

console.log(range(0, 10, 2).next());
//Object {
//  "done": false,
//  "value": 0
//}

for (let i of range(0, 10, 2)) {
    console.log(i) // 0, 2, 4, 6, 8
}






No comments:

Post a Comment