ES6 of the Week

This Week's Episode:

generators and iterators

Iterator protocol

  • MDN: An object is an  iterator  when it knows how to access items from a collection one at a time, while keeping track of its current position within that sequence 
  • An iterator is an object that knows about a specific collection of data (ex. items in an array)
  • An iterator has a method that, when invoked, will iterate to the next item in that collection and return it

Iterating

  • An iterator has a next method
  • The next method returns an object that looks like this:
    • {value: someValue, done: boolean}
  • value is the value of the current item in the collection
  • done is a boolean for whether the collection is complete or not
  • Each call to next moves the iterator on to the next item in its collection
  • So, how is an iterator associated with a collection?
let array = [1, 2, 3];
let iteratorForArray = {};

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

Iterable protocol

  • MDN: An object is an  iterable  if it defines its iteration behavior
  • Put simply: an object/collection is an iterable if it contains a special property (called Symbol.iterator) that returns its iterator
  • Strings and Arrays are iterables ( built-in iterables), as are several other new collection types in ES6
  • There are several consumers of iterables, such as the new  for...of loop

Using iterators

'use strict';

// an array is a built-in iterable
let numberArray = [1, 2, 3];
// Symbol.iterator is the special property that contains a function that returns an iterator
let it = numberArray[Symbol.iterator]();

// we can then use the iterator to manually iterate through the array's values
it.next(); // Object { value: 1, done: false }
it.next(); // Object { value: 2, done: false }
it.next(); // Object { value: 3, done: false }
it.next(); // // Object { value: undefined, done: true }

let stringArray = ['a', 'b', 'c'];

// the for...of loop consumes an iterable
for (let ch of stringArray) {
    console.log(ch) // a b c
}

Roll your own

let strArray = ['a', 'b', 'c'];
strArray[Symbol.iterator] = function () {
    let idx = 0,
        collection = this,
        len = this.length;
    
    return {
        next: function () {
            if (idx < len) {
                let value = collection[idx];
                idx++;
                return {
                    value: value + '!',
                    done: false
                };
            } else {
                return {
                    value: undefined,
                    done: true
                };
            }
        }
    };
};

for (let ch of strArray) {
    console.log(ch); // a! b! c!
}

Generators

  • A generator is a new type of function in ES6 that can maintain its own state - that is, it can pause, and then resume
  • Generator functions always return an iterator (that is, an object which adheres to the Iterator Protocol)
  • In the generator function body, the yield keyword signifies a "breakpoint" in the iterator's iteration. Whenever you call the  next method, the function executes up to the next yield keyword, and  yields that value to the  next invocation

Generation station

// we declare a generator using an asterisk
function* myGenerator (n) {
    // we yield values with the yield keyword
    yield n;
    n++;
    // we can yield as many times as we want
    // each yield is a "pause" in the generator function's execution
    yield n;
}

/* 
invoking a generator function returns an iterator!
we've passed in a value for the parameter "n", but
no other code in the generator has executed yet
*/
let myIterator = myGenerator(41);

// each call to next advances us to the next yield
let firstYield = myIterator.next();
console.log(firstYield.value); // 41
let secondYield = myIterator.next();
console.log(secondYield.value); //42

Call and Answer

// because we can pause generators, it doesn't matter if they run forever
function* generateDoubles (initialValue) {
    let x = initialValue;
    while (true) {
    	x = yield x * 2;
    }
    /*
    Something really cool is happening here!
    We can yield on the right side of an assignment.
    When we call .next on the iterator, we can pass in a value as a parameter.
    That value will then be the value that gets assigned!
    */
}

let i = 1,
    it = generateDoubles(1), // invoking the generator 'primes' it with the parameter value
    two = it.next().value, // this first call to .next advances us to the yield
    four = it.next(two).value, // each subsequent call picks up where the last yield left off!
    eight = it.next(four).value;

/*
  Each yield pauses the function and yields a value.
  If the yield is in an assignment, it will also wait
  to receive a value from outside as well!
*/

Async Await

function getData (gen) {
  gen.next();
  return Promise
    .resolve('data')
    .then(data => gen.next(data));
}

function * asyncAwait () {
  let data = yield;
  console.log(data);
}

let gen = asyncAwait();
getData(gen);

You may be wondering...

  • Can generators be used as function expressions  (i.e. used in callbacks, variables, and as object methods)?
  • Can I use apply/call/bind with generators?
    • Yes to all of the above!
  • Is there an arrow function version of generators
    • Nope!

ES6 of the Week - 8

By Tom Kelly

ES6 of the Week - 8

Generators and Iterators

  • 1,362