Iterators and generators

Refer to advanced programming 4

iterator

The for loop is the simplest iteration. The loop is the basis of the iteration mechanism. The loop can specify the number of iterations. The iterations are carried out on an ordered set, such as an array.

The array has a known length and can be obtained by index, so it can be traversed incrementally during the loop. For several reasons, it is not ideal to execute the routine through the loop.

  1. You need to know the data structure before iteration. You can only get the value through []. This method is not suitable for all data structures
  2. Traversal order is not inherent in data structures and is not suitable for other data structures with implicit order

es5 new higher order function forEach was born to solve this problem, but it can not identify when to terminate the iteration

Iterator pattern and iterative protocol

  1. Some structures are called "iteratable objects" because they implement the formal iteratable interface
  2. Any data structure that implements the iteratable interface can be implemented as the structure of the Iterator interface
  3. Many data have built-in Iterator interfaces, such as string, array, mapping, nodeList and arguments,
let str = 'abc'
let arr = ['a', 'b', 'c']
console.log(str[Symbol.iterator])
console.log(arr[Symbol.iterator])
// f values() { [native code] } 
// f values() { [native code] }

iterator protocol
You can call the iterator through next() to return an IteratorResult object. IteratorResult has two attributes, done and value. Done returns a Boolean value to determine whether there is a value. When done is true, it indicates that the state is exhausted and cannot be iterated again. Value is the next value of the iteration object

const arr = [1, 2]
const iter = arr[Symbol.iterator]()

console.log(iter.next()); // { done: false, value: 1 } 
console.log(iter.next()); // { done: false, value: 2 } 
console.log(iter.next()); // { done: true, value: undefined }

Each iterator represents a one-time ordered traversal of the iteratable object. Instances of different iterators are not related to each other, but are unique
Site traversal of iteratable objects

const arr = [1, 2]
const iter = arr[Symbol.iterator]()
const iter2 = arr[Symbol.iterator]()

console.log(iter.next()); // { done: false, value: 1 } 
console.log(iter.next()); // { done: false, value: 2 } 
console.log(iter2.next()); // { done: false, value: 1 } 
console.log(iter2.next()); // { done: false, value: 2 } 

Custom iterator

class Counter { 
    constructor(limit) { 
        this.limit = limit; 
    } 
    [Symbol.iterator]() { 
        let count = 1, 
        limit = this.limit; 
        return { 
            next() { 
                if (count <= limit) { 
                    return { done: false, value: count++ }; 
                } else { 
                    return { done: true, value: undefined }; 
                } 
            },
            return () {
                return { done: true }; 
            }
        }; 
    } 
}
for (const item of counter) {
    if (item > 2) {
        continue
    }
    console.log(item) // 1, 2
}

generator

  1. The generator can pause and resume code execution in functions
  2. Add a "*" in front of the function name to represent the generator. When the generator is implemented, it is suspended at the beginning, and execution will resume when next is called
  3. The code before yield will be executed normally. When yield is encountered, it will be suspended, and the scope state in the function will be retained. The code after yield will be called only when next is called
// Return has the same effect as yield, but yield can write multiple returns. If there are multiple returns, the following code will not be executed, and only the first return will be executed
function* generatorFn(opt) {  
    yield opt; 
    yield '1'; 
    return '2';  
    return '3'; 
}
// 

const fn = generatorFn('123')
console.log(fn.next(), fn.next(), fn.next(), fn.next())
// {value: '123', done: false} 
// {value: '1', done: false} 
// {value: '2', done: true} 
// {value: undefined, done: true}

console.log(bbb.next()) 
// {value: '123', done: false} 

Generators as iteratable objects

function* nTimes(n) { 
    while(n--) {  
        yield n; 
    } 
}
for (const i of nTimes(3)) { 
    console.log(i)   // Get value directly, instead of {done, value}
}

function* generatorFn (opt) {
    console.log(yield) // Used as an intermediate parameter of a function
    console.log(yield)
    console.log(yield)
}

// If there is nothing after the first line of yield, it returns 0, and there is nothing after other yields, it returns undefined
const generfn = generatorFn()
generfn.next() // 0
generfn.next() // undefined
generfn.next() // undefined


function* generator (opt) {
    console.log(opt) 
    console.log(yield) 
    console.log(yield)
}

const gener = generator(123)
gener.next(321) // 123 because the generator is being called
gener.next(456) // 456
gener.next(789) // 789

The yield keyword can be used for both input and output

const gener = generator()
console.log(gener.next(), gener.next(456))
// {value: '123', done: false} 
// {value: 456, done: true}

// The function needs to evaluate the expression to determine the returned value. When yield is encountered, it calculates the value 123,
// In the next call, 456 is passed in and handed over to the same yield for processing,
// 456 is then determined as the value produced by the generator function

Strengthen yield with "*"

function* generatorFn2 () {
    yield* [1, 2, 3]

    // Equivalent to
    // for (const i of [1, 2, 3]) {
    //     yield i
    // }
}
console.log(Array.from(generatorFn2())) // [1, 2, 3]

Call the generator with yield *

function* innerFn() { 
    yield 'foo'; 
    return 'bar'; 
} 

function* outerFn(genObj) { 
    console.log('iter value:', yield* innerFn()); 
} 

for (const x of outerFn()) { 
    console.log('value:', x); 
}

Generator as default iterator

// The generator function generates a generator function
class Foo { 
    constructor (opt) {
        this.opt = opt
    }
    * [Symbol.iterator] () {
        yield* this.opt
    }
}
for (const key of new Foo([1, 2, 3])) {
    console.log(key)
}

Termination generator
Use throw(), return() to terminate the generator

// All generators have return() to enter the end state in advance, and subsequent calls are done: true
function * Fn () {
    for (const item of [1, 2, 3]) {
        yield item
    }
}

const fn = Fn()
console.log(fn.next()) // {value: 1, done: false}
console.log(fn.return(666)) // {value: 666, done: true} value is undefined by default

function* Fn2() { 
    for (const x of [1, 2, 3]) { 
        yield x
    } 
} 
const g = Fn2()

for (const x of g) { 
    if (x > 1) { 
        g.return(4); 
    } 
    console.log(x); // 1, 2
}

// Calling throw will throw an error. If you handle this error, you will continue to execute below
function* Fn2() { 
    yield* [1, 2, 3]
}

const fn2 = Fn2()
try {
    fn2.throw('sleepy')
} catch (err) {
    console.log(err) // sleepy
}

// If this error is handled inside the generator, the corresponding item will be skipped when throw is called. If throw is placed in front of all next, the following will not be executed
function* Fn3() { 
    for (const item of [1, 2, 3]) {
        try {
            yield item
        } catch {}
    }
}

const fn3 = Fn3()
console.log(fn3.next()) // 1
fn3.throw('sleepy')         // yield throws an error and will not be in output 2
console.log(fn3.next()) // 3

Tags: Javascript Front-end ECMAScript

Posted on Sat, 04 Dec 2021 14:51:21 -0500 by frymaster