Iterators and Generators

Javascript has a number of iteration tools for-loop, iterator pattern and Generators which i will cover in this section.

Iteration

The for-loop is the traditional tool for iteration, however Javascript does have some others like the iterator pattern which I am going to cover.

Basic for-loop example
for (let i = 1; i <= 10; ++i) {
    console.log(i);
}

let collection = ['foo', 'bar', 'baz'];
for (let index = 0; index < collection.length; ++index) {
    console.log(collection[index]);
}
Collection forEach() example
let collection = ['foo', 'bar', 'baz'];
collection.forEach((item) => console.log(item));

Iterator Pattern

Iterator pattern is one of the design patterns from the gang of four, it describes something that is iterable and can inplement a formal Iterable interface and be consumed by a Iterator. The Object created would return a Iterator that would be able to iterate over the elements inside the Object. Built-in objcts likes Strings, Arrays, Maps/Sets already have a iterator.

Check existence of iterator
// These types do not have iterators
let num = 1;
let obj = {};
console.log(typeof num[Symbol.iterator] === 'function'); // false
console.log(typeof obj[Symbol.iterator] === 'function'); // false

// These types all have iterators
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
console.log(typeof str[Symbol.iterator] === 'function'); // true
console.log(typeof arr[Symbol.iterator] === 'function'); // true
console.log(typeof map[Symbol.iterator] === 'function'); // true
console.log(typeof set[Symbol.iterator] === 'function'); // true
Iterators in action
// for...of loops
let arr = ['foo', 'bar', 'baz'];
for (let ele of arr) {                               // foo, bar, baz
    console.log(ele);
}

// Array destructuring
let [a, b, c] = arr;
console.log(a, b, c);                               // foo, bar, baz

// Spread operator
let arr2 = [...arr];
console.log(arr2);                                  // ['foo', 'bar', 'baz']

// Array.from()
let arr3 = Array.from(arr);
console.log(arr3);                                  // ['foo', 'bar', 'baz']

// Set constructor
let set = new Set(arr);
console.log(set);                                   // Set(3) {'foo', 'bar', 'baz'}

// Map constructor
let pairs = arr.map((x, i) => [x, i]);
console.log(pairs);                                 // [['foo', 0], ['bar', 1], ['baz', 2]]

let map = new Map(pairs);
console.log(map);                                   // Map(3) { 'foo'=>0, 'bar'=>1, 'baz'=>2 }
Obtain and Use a Iterator
let names = ["Paul", "Will", "Moore", "Graham"]
let iter = names[Symbol.iterator]();    // obtain the iterator

console.log(iter.next());      // { value: 'Paul', done: false }
console.log(iter.next());      // { value: 'Will', done: false }
console.log(iter.next());      // { value: 'Moore', done: false }
console.log(iter.next());      // { value: 'Graham', done: false }
console.log(iter.next());      // { value: undefined, done: true } - done is now true, no more elements
Custom Iterator
class Counter {
    // Counter instance should iterate <limit> times
    constructor(limit) {
        this.count = 1;
        this.limit = limit;
    }

    next() {
        if (this.count <= this.limit) {
            return { done: false, value: this.count++ };
        } else {
            return { done: true, value: undefined };
        }
    }
    
    reset() {
        this.count = 1;                 // reset the count back to one
    }

    [Symbol.iterator]() {
        return this;
    }
}

let counter = new Counter(3);
for (let i of counter) {
    console.log(i);
}

counter.reset();                        // need to reset the the count back to 1

console.log(counter.next());
console.log(counter.next());
console.log(counter.next());
console.log(counter.next());

Output
--------------------------------------------------
1
2
3
{ done: false, value: 1 }
{ done: false, value: 2 }
{ done: false, value: 3 }
{ done: true, value: undefined }

Generators

Generators allow you to pause and resume code execution inside a function block, below is a how they work


Generators that the form of a function, and use a asterisk to indicate its a generator, they implement an Iterator interface which means they have a next() method, which begins or resumes the execution, the yield keyword allows the generator to stop and start execution, when the generator encounters the yield keyword the execution will halt and the scope of the function will be preserved, it will only start again then next() is called.

Basic Generator
function* generatorFn() {           // notice the asterisk
    console.log("foo");
    yield 'foo';

    console.log("bar");
    yield 'bar';

    console.log("baz");
    return 'baz';
}

let generatorObject1 = generatorFn();
let generatorObject2 = generatorFn();

console.log(generatorObject1.next()); // { done: false, value: 'foo' }
console.log(generatorObject2.next()); // { done: false, value: 'foo' }

// At this point we are positioned at console.log("bar") ready to continue

console.log(generatorObject1.next()); // { done: false, value: 'bar' }
console.log(generatorObject2.next()); // { done: false, value: 'bar' }

// At this point we are positioned at console.log("baz") ready to continue

console.log(generatorObject1.next()); // { done: false, value: 'baz' }
console.log(generatorObject2.next()); // { done: false, value: 'baz' }


Output
-----------------------------------------------------------------
foo
{ value: 'foo', done: false }
foo
{ value: 'foo', done: false }
bar
{ value: 'bar', done: false }
bar
{ value: 'bar', done: false }
baz
{ value: 'baz', done: true }
baz
{ value: 'baz', done: true }
Generator example
// Infinite counting generator function, util program restarts
function* generatorFn() {
    for (let i = 0;;++i) {
        yield i;
    }
}

let generatorObject = generatorFn();
console.log(generatorObject.next().value); // 0
console.log(generatorObject.next().value); // 1
console.log(generatorObject.next().value); // 2
console.log(generatorObject.next().value); // 3
console.log(generatorObject.next().value); // 4
console.log(generatorObject.next().value); // 5
Yielding an Iterable
// generatorFn is equivalent to:
// function* generatorFn() {
//     for (const x of [1, 2, 3]) {
//         yield x;
//     }
// }

function* generatorFn() {
    yield* [1, 2, 3];                            // Notice the asterisk
}

let generatorObject = generatorFn();
for (const x of generatorFn()) {                 // results in 1, 2, 3
    console.log(x);
}
Returning early
function* generatorFn() {
    for (const x of [1, 2, 3]) {
        yield x;
    }
}

const g = generatorFn();
console.log(g);                               // generatorFn {<suspended>}
console.log(g.return(4));                     // { done: true, value: 4 }
console.log(g);                               // generatorFn {<closed>}

Note: you can also use throw()