Iterators and generators for JavaScript

ECMAScript6 adds two advanced features: iterators and generators.

iterator

Loop is the basis of iterative mechanism. The iteration will take place on an ordered set.

The iteration of the for loop is not ideal:

  • Know how to use data structures in advance.
  • Traversal order is not inherent in data structures.

Iterator mode

Iterator pattern: some data structures are called iteratable objects. They implement a formal iteratable interface and can be consumed through iterators.

Iterative protocol

Two capabilities are required to implement Iterable interface (iterative protocol):

  • Support iterative self identification.
  • The ability to create objects that implement the Iterable interface.

The iteratable object must expose a property as the default iterator. This property takes symbol.iteratable as the key and the value is an iterative factory function, which returns a new iterator.
This attribute is not required to be displayed when actually writing code. The native language features that accept iteratable objects include:

  • for-of
  • Array deconstruction
  • Extension operator
  • Array.form()
  • Create collection
  • Create mapping
  • Promise.all() receives an iteratable object composed of promise.
  • Promise.race() receives an iteratable object composed of promise.
  • yield * operator. Used in the generator.

iterator protocol

An iterator is a one-time object used to iterate over the iteratable objects associated with it. The iterator API uses the next() method to traverse the data in an iteratable object. Each successful call to next() returns an IteratorResult
Object containing the next value returned by the iterator. If you do not call next(), you cannot know the current location of the iterator.

The IteratorResult object contains 2 properties:

  • Whether done is consumed. The consumed value is true.
  • Value iterates over the next value of the object.
const p = [1, 2, 3, 4, 5, 6];
const iterable = p[Symbol.iterator]();

console.log(iterable.next()) // { value: 1, done: false }
console.log(iterable.next()) // { value: 2, done: false }
console.log(iterable.next()) // { value: 3, done: false }
console.log(iterable.next()) // { value: 4, done: false } 
console.log(iterable.next()) // { value: 5, done: false }
console.log(iterable.next()) // { value: 6, done: false }
console.log(iterable.next()) // { value: undefined, done: true }

The iterator maintains a reference to the iteratable object, which prevents the garbage collector from recycling the iterated object.

Custom iterator

[Symbol.iterator] the return can include two methods:

  • next() the value returned for each iteration. {value:any,done:boolean}
  • return() is called when you exit the iteration ahead of time. {done: true}
class Count {
    constructor(limit = 10) {

        this.limit = limit;
    }

    [Symbol.iterator]() {
        let count = 1,
            limit = this.limit;
        return {
            next() {
                if (count <= limit) {
                    return {value: count++, done: false};
                }
                return {value: undefined, done: true};
            },
            return() {
                console.log("Early termination");
                return {value: undefined, done: true};
            }
        };
    }
}

let count = new Count(5);
for (const item of count) {
    console.log(item)
    if (item === 4) {
        break;
    }
}

generator

The generator is an extremely flexible structure with the ability to pause and resume execution within a function block.
Using the generator, you can generate a coroutine from a custom iterator and.

Create a generator

The form of generator is a function, and a * sign before the function name indicates that it is a generator. The generator function returns a generator object. The generator also has an Iterator object.
The generator executes with a yield interrupt.

function* gen() {
    yield 1;  // Interrupt execution through yield.
    yield 2;
    yield 3;
    return 4;
}

const genVal = gen();
console.log(genVal === genVal[Symbol.iterator]()); // true
console.log(genVal.next()); // { value: 1, done: false }
console.log(genVal.next()); // { value: 2, done: false }
console.log(genVal.next()); // { value: 3, done: false }
console.log(genVal.next()); // { value: 4, done: true }
console.log(genVal.next()); // { value: undefined, done: true }

yield implements input and output

yield can be used as an intermediate return statement or as an intermediate parameter of a function.

function* gen2(v) {
    console.log("first next");
    const a = yield v;       // a = 3
    console.log("the second next");
    const b = yield 5 + a;   // b = 4
    console.log("Third next");
    const c = yield 5 + b;   // c = 5
    console.log("Fourth next");
    return c;
}

const g2 = gen2(1);
console.log("start");
console.log(g2.next(2)) // {value: 1, done: false} the value of the first next will not be used.
console.log(g2.next(3)) // {value: 8, done: false} This value is passed to the first yield
console.log(g2.next(4)) // {value: 9, done: false} This value is passed to the second yield
console.log(g2.next(5)) // {value: 5, done: false} This value is passed to the third yield

Early termination generator

Both return() and throw() can prematurely terminate the generator.


function* gen3() {
    try {
        yield 1;
    } catch (e) { // e accept throw parameters
        console.log(e)
        return e;
    }
    yield 2;
    yield 3;
}

const g3 = gen3();

console.log(g3.next()) // { value: 1, done: false }
console.log(g3.return(10)) // {value: 10, done: true} early termination
console.log(g3.next()) // { value: undefined, done: true }


const g4 = gen3();

console.log(g4.next()) // { value: 1, done: false }
console.log(g4.throw(10)) // {value: 10, done: true} early termination
console.log(g4.next()) // { value: undefined, done: true }

Keywords: Javascript

Added by Murciano on Mon, 20 Sep 2021 06:02:24 +0300