How to handle javascript generators and iterators

Basics

Generators were introduced in ES6. Iterators know how to access single element of collection, on the other hand generators allows for easier creation of iterators.

Below is example of generator function, notice * before function name and yield keyword.

function *names() {
    yield 'Jhon';
    yield 'Mack';
    return 'Kate';
}

Now when we will call names() it will return iterator which can be used as below.


const itr = names();

console.log(itr.next()); // { value: 'Jhon', done: false }
console.log(itr.next()); // { value: 'Mack', done: false }
console.log(itr.next()); // { value: 'Kate', done: true }
console.log(itr.next()); // { value: undefined, done: true }

next function of iterator return object with two keys value and done. done informs us if iterator has another value to return. Calling next when iterator has no more values will result with undefined value of value field.

Now, what happened here, is that yield returns value to next() function of iterator. We have used yield twice, also we have one return statement that means that we call call next 3 times, after that iterators is done.

As you can see we can make generators which return infite values like below example.

function *countToInfinity() {
    let i = 0;
    while(true) {
        yield i++;
    }
}

Iterator returned by countToInfinity will always have done field set to false because of infinite loop.

With iterators and generators we are also allowed to pass in values to the next call.

function *sayHello() {
    const name = yield 'Jack';
    return `Hello ${name}`
}

const itr = sayHello();

console.log(itr.next()); // { value: 'Jack', done: false }
console.log(itr.next('Michel')); // { value: 'Hello Michel', done: true }

yield keyword has two functions first is to return value to iterator, and second is to pass value from iterator to inside of function(generator).

So what happens here, is when call first itr.next(), it will go to yield in sayHello function and return value at right side, and them pause execution. After second call to next with itr.next('Michel') yield will pass ‘Michel’ string as value to name variable and go to line with return statement.

If we do not need to return jack name from sayHello() then we can simplify it to this:

function *sayHello() {
    const name = yield;
    return `Hello ${name}`
}

const itr = sayHello();

console.log(itr.next()); // { value: undefined', done: false }
console.log(itr.next('Michel')); // { value: 'Hello Michel', done: true }

or even to:

function *sayHello() {
    return `Hello ${yield}`
}

Asynchronous operations (with promises)

Generators and iterators can alo be used in asynchronous operations.

Here is simple example with usage of co library.

const co = require('co');

function delay(delayMs) {
    return new Promise(resolve => {
        setTimeout(() => resolve(`delayed by ${delayMs} ms`), delayMs)
    })
}

co(function *(){
    var a = delay(600);
    var b = delay(200);
    var c = delay(500);
    var res = yield [a, b, c];
    console.log(res); // [ 'delayed by 600 ms', 'delayed by 200 ms', 'delayed by 500 ms' ]
});

co function takes in generator function and resolves promises inside it, thanks to this we are able to use promises as they would be synchronous functions which return values.

Currently with es7 for asynchronous operations we ca use async/await keywords with instead of generators, but if we are stuck with older versions of nodejs (e.g when using AWS lambda) generators are the way to go.