Iterators and Generators
Iterators
An iterable object is any object with a special iterator method that returns an iterator object. An iterator is any object with a next()
method that returns an iteration result object. An iteration result object is an object with properties named value
and done
.
To iterate an iterable object, you first call its iterator method Symbol.iterator
to get an iterator object. Then, you call the next()
method of the iterator object repeatedly until the returned value has its done
property set to true
.
let list = [1,2,3,4,5];
let iter = list[Symbol.iterator]();
let head = iter.next(); // => {value: 1, done: false}
for (let res = iter.next(); !res.done; res = iter.next()) {
console.log(res.value, res.done);
}
Implementing iterable objects
In order to make a class iterable, you must implement a method with computed property name [Symbol.iterator]()
. That method must return an iterator object that has a next()
method. And the next()
method must return an iteration result object that has value
property and/or a boolean done
property.
class Range {
// Iterable class Range
constructor(from, to) {
this.from = from;
this.to = to;
}
// Must implement a method named [Symbol.iterator]()
[Symbol.iterator]() {
let next = Math.ceil(this.from);
let last = this.to;
// Must return an iterator object with a method next()
return {
next() {
if (next < last) {
return { value: next++ };
} else {
return { done: true };
}
}
};
}
}
for (let i of new Range(1,3)) {
console.log(i);
}
[...new Range(-1,2)]; // => [-1, 0, 1]
Iterator object may implement a return()
method to go along with the next()
method. If iteration stops before next()
has returned an iteration result with done
property set to true
, then the interpreter will invoke a return()
method (if it exists). The return()
method must return an iterator result object, however the properties of the object are ignored.
class RangeIterator {
constructor(start, end) {
this.current = start;
this.end = end;
}
next() {
if (this.current <= this.end) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
}
return() {
console.log('Iterator is being closed early');
return { value: undefined, done: true };
}
[Symbol.iterator]() {
return this;
}
}
// Example usage
const range = new RangeIterator(-3, 3);
for (const value of range) {
console.log(value); // Logs -3, -2, -1, 0, then breaks early
if (value === 0) {
break;
}
}
Generators
A generator is special type of function that can pause its execution and subsequently resume from where it paused. To create a generator, you define a generator function with the keyword function*
.
When you invoke a generator function, it does not execute the function body, but instead returns a generator object, which is an iterator. Calling its next()
method causes the body of the generator function to run from the start to the yield
statement. The yield
statement pauses the generator function, returning the value and waiting for the next invocation of the next()
method. Each call to next()
method resumes the function from where it left off, continues to the next yield
statement, and returns the next number in the sequence. yield
does not work with arrow functions.
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
let fibonacciGenerator = fibonacci();
fibonacciGenerator.next().value; // => 1
fibonacciGenerator.next().value; // => 1
fibonacciGenerator.next().value; // => 2
fibonacciGenerator.next().value; // => 3
fibonacciGenerator.next().value; // => 5
fibonacciGenerator.next().value; // => 8
You can create a generator function that combines the elements of multiple iterable objects.
function* zip(...iterables) {
let iterators = iterables.map(i => i[Symbol.iterator]());
let index = 0;
while (iterators.length > 0) {
if (index >= iterators.length) {
// if last iterator is reached go to first one
index = 0;
}
// get the next item from the iterator
let item = iterators[index].next();
if (item.done) {
// if the iterator is done, remove it from array
iterators.splice(index, 1);
} else {
// yield iterator value
yield item.value;
// move to the next iterator
index++;
}
}
}
[...zip([1,2,3], 'abc', new Range(-3,0))];
// => [1, 'a', -3, 2, 'b', -2, 3, 'c', -1]
The yield*
keyword is like yield
except that it iterates an iterable object and yields each of the resulting values.
function* sequence(...iterables) {
for (let iterable of iterables) {
yield* iterable;
}
}
[...sequence('abc', new Range(0, 3))];
// => ['a', 'b', 'c', 0, 1, 2]
In classes and object literals you can use a shorthand notation of a generator function by using an asterisk before the method name.
let o = {
x: 1, y: 2, z: 3,
*g() {
yield* Object.keys(this);
}
};
[...o.g()]; // => ['x', 'y', 'z', 'g']