ES6 Generator function sweet and sour parts

With ES6 generators, we have a different kind of function, which may be paused in the middle, one or many times, and resumed later, allowing other code to run during these paused periods.

The function* declaration (function keyword followed by an asterisk) defines a generator function, which returns a Generator object.

function* foo() {
    
}

Note: Another way of specifying generator function is function *foo, where the asterisk is next to the function name.

yeild keyword

Every time a generator function encounters yeild keyword, it exits the function and returns an object {value: <value>, done: <boolean>}

function* foo() {
    yeild 1;
    yeild 2;
}

ES6 generator functions are “cooperative” in their concurrency behavior. Inside the generator function body, you use the new yield keyword to pause the function from inside itself. Nothing can pause a generator from the outside; it pauses itself when it comes across a yield.

However, once a generator has yield-paused itself, it cannot resume on its own. An external control must be used to restart the generator.

Calling generator function

Calling generator function returns an object, which can be used to resume generator function execution.

function* foo() {
    yield 1;
    yield 2;
}

const fn = foo();

> fn
{[[GeneratorStatus]]: “suspended”}

To get the value out of generator function next() can be called on the object returned by the generator function. value is returned untill all the yeild statements are reached. It also returns done indicating the status of generator function.

fn.next();  // {value: 1, done: false}
fn.next();  // {value: 2, done: false}
fn.next();  // {value: undefined, done: true}

return in generator functions

Using return in generator function returns a value and subsequent statements after return are unreachable.

Also, the done is set to true after the return statement is encountered.

function* foo() {
    return 1;
}

const fn = foo();

fn.next();  // {value: 1, done: true}

Using return value from generators, when iterating generator functions with for..of loops, the final returned value would be thrown away.

Generator functions within loops

A generator function is iterable. Its automatically iterated within a for..of loop. So, there is no need for us to explicitly get the next item.

function *fibonacci() { // a generator function
    let [prev, curr] = [1, 1];
    let index = 0;
    while ((index++) < 10) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}

for (let n of fibonacci()) {
    console.log(n);
}

// 2, 3, 5, 8, 13, 21, 34, 55, 89, 144

Iteration protocols

There are two protocols:

  • The iterable protocol
  • The iterator protocol

The iterable protocol

The iterable protocol allows JavaScript objects to define or customize their iteration behavior, such as what values are looped over in a for..of construct.

Some built-in types are built-in iterables with a default iteration behavior, such as Array or Map, while other types (such as Object) are not.

In order to be iterable, an object must implement the @@iterator method, meaning that the object (or one of the objects up its prototype chain) must have a property with a @@iterator key which is available via constant Symbol.iterator.

Symbol.iterator A zero arguments function that returns an object, conforming to the iterator protocol.

Whenever an object needs to be iterated (such as at the beginning of a for..of loop), its @@iterator method is called with no arguments, and the returned iterator is used to obtain the values to be iterated.

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

for(var item of myIterable) { 
    console.log(item) 
}
// 1
// 2
// 3

The iterator protocol

The iterator protocol defines a standard way to produce a sequence of values (either finite or infinite). An object is an iterator when it implements a next() method with the following semantics.

A zero arguments function that returns an object with two properties:

  • done (boolean)
    • Has the value true if the iterator is past the end of the iterated sequence. In this case value optionally specifies the return value of the iterator. The return values are explained here.
    • Has the value false if the iterator was able to produce the next value in the sequence. This is equivalent of not specifying the done property altogether.
  • value – any JavaScript value returned by the iterator. Can be omitted when done is true.

Example of finite iterator custom function

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

var it = makeIterator(['yo', 'ya']);

console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done);  // true

Is a generator object an iterator or an iterable?

A generator object is both, iterator and iterable:

var aGeneratorObject = function*(){
    yield 1;
    yield 2;
    yield 3;
}();
> typeof aGeneratorObject.next;    
// "function", because it has a next method, so it's an `iterator`
> typeof aGeneratorObject[Symbol.iterator];     
// "function", because it has an @@iterator method, so it's an `iterable`
aGeneratorObject[Symbol.iterator]() === aGeneratorObject;
// true, because its @@iterator method return its self (an iterator), so it's an well-formed iterable
[...aGeneratorObject];
// [1, 2, 3]

https://davidwalsh.name/es6-generators