OdeToCode IC Logo

ECMAScript 2015 Iterators Revisited

Tuesday, May 5, 2015

In an earlier post, we saw how to work with iterators at a low level and use an iterator’s next method to move from one item to the next item. What’s interesting about iterators in JavaScript is how the consumer of an iterator can influence the internal state of the iterator by passing a parameter to the next method.

As an example, let’s look at the following range method which you can use to generate a sequence of numbers from start to end.

let range = function*(start, end) {

    let current = start;

    while(current <= end) {
        yield current;
        current += 1;
    }
}

If we ask range to give us numbers from one to ten, the range method will behave as expected.

let result = [];
let iterator = range(1,10);
let next = iterator.next();

while(!next.done) {
    result.push(next.value);
    next = iterator.next();
}

expect(result).toEqual([1,2,3,4,5,6,7,8,9,10]);

Now let’s make a small change to the range method. When we yield the current value, we’ll place the yield statement on the right-hand side of an assignment expression.

let range = function*(start, end) {
    let current = start;

    while(current <= end) {
        var delta = yield current;
        current += delta || 1;
    }
}

The range generator now has the ability to take a parameter from the consumer of the iterator and use this parameter to calculate the next value. If the consumer does not pass a value, the code defaults the next increment to 1, otherwise the code uses the value passed to retrieve the next value. If we iterate over range like we did before we will see the same results.

let result = [];
let iterator = range(1,10);
let next = iterator.next();

while(!next.done) {
    result.push(next.value);
    next = iterator.next();
}

expect(result).toEqual([1,2,3,4,5,6,7,8,9,10]);

Passing a parameter to next is optional, however, with the following code we’ll pass the number 2 on each call to next, which effectively increments the current value in the iterator by two instead of one.

let result = [];
let iterator = range(1,10);
let next = iterator.next();

while(!next.done) {
    result.push(next.value);
    next = iterator.next(2);
}

expect(result).toEqual([1,3,5,7,9]);

We could also pass the current value to next and produce a more interesting sequence.

let result = [];
let iterator = range(1,10);
let next = iterator.next();

while(!next.done) {
    result.push(next.value);
    next = iterator.next(next.value);
}

expect(result).toEqual([1,2,4,8]);

If we were to write the range method the hard way instead of using yield, it might come out to look like the following.

let range = function(start, end) {

    let firstCall = true;
    let current = start;

    return {

        next(delta = 1) {

            let result = { value: undefined, done: true};

            if(firstCall){
                firstCall = false;
            }
            else {
                current += delta;
            }

            if(current <= end) {
                result.value = current;
                result.done = false;
            }
            
            return result;
        }
    }
}

In this version of the code it is easy to see how the next method receives a parameter you can use in the iterator logic. When using yield, the parameter arrives as the return value of the yield expression. Also note that when implementing the range function using yield there is no ability to grab a parameter on the first call to the next method. The first call to next starts the iteration and returns a value, but the parameter received by the first yield in a generator method will be the value passed to the second invocation of next.