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,440