• Iteration. Iteration.
  • To execute a program several times in sequence.

1. Understand iteration

  • Arrays can be traversed by increasing indexes, but this is not ideal
    • Do you know how to use data structures before you iterate
    • The traversal order is not inherent in the data structure
  • New Array ES5. Prototype. The forEach ()
    • Solve the problem of measuring indexes separately and fetching values from array objects
    • There is no way to tell when an iteration terminates, only for arrays.
  • Iterator mode: Developers do not need to know how to iterate to implement iterative operations, which is supported in ES6.

2. Iterator pattern

  • The Iterator pattern describes a scenario where structures can be called “iterables” because they implement the formal Iterabel interface and can be consumed through the Iterator Iterator.
  • An iterable can be understood as an object of the collection type, such as an array or a collection. Contains a finite number of elements, all with an unambiguous traversal order.
  • An iterable does not have to be a collection object, but can be any other data structure that simply behaves like an array. The values generated in the loop are transient, but the loop itself is iterating. Counting loops and arrays both have the behavior of an iterable.
  • Temporary iterables can be implemented as generators.
  • Any data structure that implements Iterable can be “consumed” by the structure that implements Iterator. Iterators are disposable objects that are created on demand. Each iterator is associated with an iterable, and iterators expose the ITERator’s associated iterable API. An iterator does not need to know the structure of the iterable it is associated with, only how to obtain continuous values.

1. Iterative protocol

  • Implementing the Iterable interface S requires two capabilities

    • Support for iterative self-identification
    • The ability to create objects that implement the Iterator interface
  • A property must be exposed as the “default iterator”

  • This property must use a special Symbol. Iterator as the key

  • The default iterator property must reference an iterator factory function

  • Calling this factory function must return a new iterator.

  • Many of the built-in types implement the Iterable interface. Check to see if there are default iterator attributes that expose the factory function

    let num = 1
    let obj = {}
    
    / / not built-in
    num[Symbol.iterator] // undefined
    obj[Symbol.iterator] // undefined
    
    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');
    let els = document.querySelectorAll('div');
    // These types all implement iterator factory functions
    // f values() { [native code] }
    str[Symbol.iterator] 
    arr[Symbol.iterator] 
    map[Symbol.iterator]
    set[Symbol.iterator] 
    els[Symbol.iterator] 
    
    // Calling the factory function generates an iterator
    str[Symbol.iterator](); // StringIterator {}
    arr[Symbol.iterator](); // ArrayIterator {}
    map[Symbol.iterator](); // MapIterator {}
    set[Symbol.iterator](); // SetIterator {}
    els[Symbol.iterator](); // ArrayIterator {}
    Copy the code
  • We don’t actually need to explicitly call this factory function to generate iterators, which receive the native language characteristics of the iterator:

    • For – loops
    • An array of deconstruction
    • Extension operator
    • Array.from()
    • Create a collection
    • Create a mapping
    • Promise.all() receives an iterable composed of terms
    • Promise.race() receives an iterable consisting of terms
    • Yield * operator, used in generators
  • Inheritable if the parent class on the object’s prototype chain implements the Iterable interface

    class FooArray extends Array {}
    let fooArr = new FooArray('foo'.'bar'.'baz')
    for(let el of fooArr){
      ...
    }
    Copy the code

2. Iterator protocol

  • An iterator is a disposable object used to iterate over the iterable associated with it.

  • Next () iterates through the data in the iterator

  • Each call to next() returns an IteratorResult object

    • Contains the next value returned by the iterator
  • There is no way to know the current iterator position without calling next()

  • The IteratorResult object contains two attributes

    • The done Boolean indicates whether next() can also be called to get the next value
    • Value contains the next value of the iterable (done is false), or undefined (done is true)
    // Iterable
    let arr = ['foo'.'bar']
    
    // Iterator factory function
    arr[Symbol.iterator]//f values(){[native code]}
    
    / / the iterator
    let iter = arr[Symbol.iterator]()
    iter // ArrayIterator()
    
    // Perform iteration
    iter.next() // {done: false, value: 'foo'}
    iter.next() // {done: false, value: 'bar'}
    iter.next() // {done: true, value: undefined}
    Copy the code
  • As long as the iterator reaches the done: true state, subsequent calls to next() will always return the same value.

  • Each iterator represents a one-time ordered traversal of the iterable, with no connection between the different iterator instances.

  • An iterator is not bound to a snapshot at any point in time, but merely a record of traversing the iterable, and if it changes during that time, the iterator will change

    let arr = [1.2]
    let iter = arr[Symbol.iteator]()
    iter.next() // {done: false, value: 1}
    
    arr.splice(1.0.0)
    iter.next() // {done: false, value: 0}
    iter.next() // {done: false, value: 2}
    iter.next() // {done: true, value: undefined}
    Copy the code
  • Iterators maintain a reference to the iterable, so iterators prevent the garbage collector from collecting the iterable.

  • “Iterator”, which can refer to a generic iteration or an excuse, can refer to a formal type of iterator.

    // This class implements Iterable interfaces.
    // Calling the default iterator factory function returns
    // An Iterator object that implements an Iterator interface
    class Foo {[Symbol.iterator]() {
      return {
       next() {
        return {done: false.value: "foo"}
          }
        }
      }
    }
    
    let f = new Foo() 
    f[Symbol.iteator]() // { next: f() {} }
    
    // The Array type implements an Iterable interface
    // Call the default iterator factory function of type Array
    // An ArrayIterator instance is created
    let a = new Array()
    a[Symbol.iterator]() // Array Iterator{}
    Copy the code

    3. Custom iterators

  • Like the Iterable interface, any object that implements the Iterator interface can be used as an Iterator.

    // Each instance can only be iterated once
    class Counter {
     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}}} [Symbol.iterator]() {
        return this}}let counter = new Counter(3)
    for(let i of counter) {
      ...
    }
    Copy the code
  • In order for an iterable to create multiple iterators, a new counter must be created for each iterator created.

    class Counter {
      constructor(limit){
        this.limit = limit
      }
      [Symbol.iterator]() {
        let count = 1;
        let limit = this.limit;
        return {
          next() {
            if(count <= limit) {
              return {done: false.value: count++}
            }else {
         return {done: true.value: undefined}
            }
          }
        }
      }
    }
    let counter = new Counter(3)
    for(let i of counter) {
      ...
    }
    for(let i of counter) {
      ...
    }
    Copy the code
  • Each iterator created in this way also implements the Iterable interface.

  • The factory function referenced by the symbol. iterator property returns the same iterator

    let arr = [1.2.3]
    let iter1 = arr[Symbol.iterator]()
    let iter2 = iter1[Symbol.iterator]()
    iter1 === iter2
    Copy the code
  • Because each iterator implements the Iterable interface, it can be used anywhere an Iterable is expected.

4. Terminate iterators early

  • The return() method is used to specify the logic to be executed when the iterator is prematurely closed.

  • The structure of performing iterations lets the iterator know that it does not want to iterate until the iterable runs out.

    • For-of exits prematurely with break, continue, return, throw
    • The deconstruction operation does not consume all values
  • The return() method must return a valid IteratorResult object.

    class Counter{
      constructor(limit) {
        this.limit = limit
      }
      
      [Symbol.iterator]() {
        let count = 1;
        let limit = this.limit;
        return {
          next() {
            if(count <= limit) {
              return {done: false.value: count++}
            }else {
         return {done: true}}},return() {
            console.log('Exiting early')
            return {done: true}
          }
        }
      }
    }
    
    let counter1 = new Counter(5)
    
    for(let i of counter1) {
      if(i > 2) {
        break
      }
      console.log(i)
    }
    
    let counter2 = new Counter(5)
    try{
      for(let i of counter2) {
        if(i > 2) {
          throw 'err'
        }
      console.log(i)
      }
    }catch(e) {}
    
    let counter3 = new Counter(5)
    let [a, b] = counter3
    
    Copy the code
  • If the iterator is not closed, we can continue iterating from where we left off last time

  • Because the return() method is optional, not all iterators can be closed. You can test if the return property is a function object. But simply adding this method to an iterator that cannot be closed does not make it closed, because calling return() does not force the iterator into a closed state. But return is still called

    let a = [1.2.3.4.5]
    let iter = a[Symbol.iterator]()
    iter.return = function () {
      console.log('Exiting early')
      return {done: true}}for(let i of iter) {
      console.log(i)
      if(i > 2) {break; }}/ / 1
    / / 2
    / / 3
    // 'Exiting early'
    
    for(let i of iter) {
      console.log(i)
    }
    / / 4
    / / 5
    Copy the code

    3. Generators

  • Generators have the ability to pause and resume code execution within a function block.

  • This allows you to customize iterators and implement coroutines

1. Generator basics

  • A generator is a function whose name is preceded by an asterisk (*) to indicate that it is a generator.

  • Generators can be defined wherever functions can be defined

    // Generator function declaration
    function* generatorFn() {}
    
    // Generator function expression
    let generatorFn = function* () {}
    
    // a generator function that acts as an object literal method
    let foo = {
      * generationFn(){}}// a generator function that acts as a class instance method
    class Foo {*generationFn(){}}// as a generator function for static methods
    class Foo {
      static * generationFn(){}}Copy the code
    • Arrow functions cannot be used to define generator functions
  • Asterisks that identify generator functions are not affected by side Spaces

    / / equivalent
    function* generatorFnA() {}
    function *generatorFnB() {}
    function * generatorFnC() {}
    
    class Foo {*generatorFnD() {}
      * genreatorFnF(){}}Copy the code
  • Calling a generator function produces a generator object.

    • It was suspended at first

    • Similar to iterators, the Iterator interface is implemented with the next() method

    • Calling this method causes the generator to start or resume execution

      function* generatorFn() {}
      const g = generatorFn()
      g // generatorFn {<suspended>}
      g.next // f next() { [native code] }
      Copy the code
    • The return value of the next() method is like an iterator, with a done attribute and a value attribute.

    • Generator functions with an empty body do not stop in the middle; a call to next() will bring the generator to the done: true state.

      function* generatorFn() {}
      let generatorObject = generatorFn();
      generatorObject; // generatorFn {<suspended>}
      generatorObject.next(); // { done: true, value: undefined }
      Copy the code
    • The value attribute is the return value of the generator function. The default value is undefined, which can be specified by the return value of the generator function

      function* generatorFn() {
       return 'foo';
      }
      let generatorObject = generatorFn();
      generatorObject; // generatorFn {<suspended>}
      generatorObject.next(); // { done: true, value: 'foo' }
      Copy the code
    • The generator function will only start execution after the first call to the next() method

      function* generatorFn() {
       console.log('foobar');
      }
      // The first call to the generator function does not print a log
      let generatorObject = generatorFn();
      generatorObject.next(); // foobar
      Copy the code
    • Generator objects implement the Iterable interface and their default iterators are self-referential

      function* generatorFn() {}
      
      generatorFn;
      // f* generatorFn() {}
      
      generatorFn()[Symbol.iterator]; 
      // f [Symbol.iterator]() {native code}
      
      generatorFn();
      // generatorFn {<suspended>}
      
      generatorFn()[Symbol.iterator]();
      // generatorFn {<suspended>}
      
      const g = generatorFn();
      g === g[Symbol.iterator]();
      // true 
      Copy the code

2. Interrupt execution by yield

  • Yield causes the generator to stop and start execution.

  • The generator function executes normally until the yield keyword is encountered.

  • Execution stops when this keyword is encountered, and the state of the function scope is preserved.

  • A stopped generator function can only resume execution by calling the next() method on the generator object.

    function* generatorFn() {
      yield;
    }
    let generatorObject = generatorFn();
    generatorObject.next() //{done: false, value: undefined}
    generatorObject.next() //{done: true, value: undefined}
    Copy the code
  • Yield is like the middle return statement of a function

  • The generated value appears in the object returned by the next() method

  • Generator functions derived from the yield keyword are in the done: false state

  • Exit via return will be in the done: true state

    function* generatorFn() {
     yield 'foo';
     yield 'bar';
     return 'baz';
    }
    let generatorObject = generatorFn();
    console.log(generatorObject.next()); 
    // { done: false, value: 'foo' }
    console.log(generatorObject.next());
    // { done: false, value: 'bar' }
    console.log(generatorObject.next()); 
    // { done: true, value: 'baz' } 
    Copy the code
  • The internal execution process is scoped for each generator object and does not affect each other.

  • Yield can only be used inside generator functions, throwing errors elsewhere, and must be located directly in the generator function definition. Occurrence in nested non-generator functions will result in an error.

    / / effective
    function* validGeneratorFn() {
     yield;
    }
    / / is invalid
    function* invalidGeneratorFnA() {
     function a() {
      yield; }}/ / is invalid
    function* invalidGeneratorFnB() {
     const b = () = > {
      yield; }}/ / is invalid
    function* invalidGeneratorFnC() {(() = > {
      yield; }) (); }Copy the code

1. Generator objects as iterables

  • Calling next() explicitly is of little use and is more convenient as an iterable.

    function* generatorFn() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    for(const x of generatorFn()) {
      console.log(x)
    }
    / / 1
    / / 2
    / / 3
    Copy the code
  • Execute specified number of times

    function* nTimes(n) {
     while(n--){
        yield}}for(let _ of nTimes(2)) {
      console.lgo('foo')}// foo
    // foo
    Copy the code

2. Use yield to implement input and output

  • Used as an intermediate function argument, the yield keyword that paused the generator last time receives the first value of the next() method.

  • The value passed in from the first call to next() is not used because the first time is to start the execution of the generator function.

    function* generatorFn(initial) {
      console.log(initial)
      console.log(yield)
      console.log(yield)}let generatorObject = generatorFn('foo')
    
    generatorObject.next('bar') // foo
    generatorObject.next('baz') // baz
    generatorObject.next('qux') // qux
    Copy the code
  • Yield can be used for both input and output

    function* generatorFn() {
     return yield 'foo'
    }
    let generatorObject = generatorFn()
    generatorObject.next() 
    //{done: false, value: 'foo'}
    generatorObject.next('bar')
    //{done: true, value: 'bar'}
    Copy the code
  • The function must evaluate the entire expression to determine the value to return, pauses execution when the yield keyword is encountered and calculates the value to produce, and the next survey, next(), passes in bar as the value to be given to the same yield. It is then determined to be the value to be returned by the generator function.

  • Yield is not used only once

    function* genreatorFn() {
     for(let i = 0; ++i){
      yieldi; }}Copy the code
  • The index of the iteration is generated based on the number of iterations of the configured value.

    function* nTimes(n) {
      for(let i = 0; i < n; i++) {
        yield i
      }
    }
    
    // while
    function* nTimes(n) {
      let i = 0
      while(n--) {
        yield i++
      }
    }
    
    for(let x of nTimes(3)) {console.log(x)
    }
    Copy the code
  • Can be used to implement ranges and fill arrays

    function* range(start, end) {
      while(end > start) {
        yield start++
      }
    }
    for(const x of range(4.7)) {console.log(x)
    }
    / / 4
    / / 5
    / / 6
    
    function* zeroes(n) {
      while(n--) {
        yield 0}}Array.from(zeroes(8))
    / /,0,0,0,0,0,0,0 [0]
    Copy the code

3. Generate iterables

  • Use asterisks to enhance yield behavior so that it can iterate over an iterable, thus producing one value at a time

    function* generatorFn() {
      for(const x of [1.2.3]) {yield x
      }
    }
    
    function* generatorFn() {
      yield* [1.2.3]}Copy the code
  • Spaces on both sides of the yield asterisk do not affect

    function* generatorFn() {
     yield* [1.2]
     yield* [3.4]
      yield * [5.6]}for(const x of generatorFn()) {
      console.log(x)
    }
    / / 1-6
    Copy the code
    • Yield * is really just a way of serializing an iterable into a series of values that can be produced individually.
  • Yield * is the property of value when the associative iterator returns done: true,

    • For normal iterators, undefined
    function* generatorFn() {
     console.log('iter value:'.yield* [1.2.3])}for(const x of generatorFn()) {
      console.log('value': x)
    }
    // value: 1
    // value: 2
    // value: 3
    // iter value: undefined
    Copy the code
    • For generator functions, this value is the value returned by the generator function
    function* innerGeneratorFn() {
     yield 'foo'
      return 'bar'
    }
    
    function* outerGeneratorFn() {
      console.log('iter value:'.yield* innerGeneratorFn())
    }
    for(const x of outerGeneratorFn()) {
      console.log('value': x)
    }
    // value: foo
    // iter value: bar
    Copy the code

4. Use yield* to implement recursive algorithms

  • The most useful thing is to implement recursive operations where generators generate themselves

    function* nTimes(n) {
      if(n > 0) {yield* nTimes(n - 1)
        yield n-1}}for(const x of nTimes(3)) {console.log(x)
    }
    / / 0
    / / 1
    / / 2
    Copy the code
  • The realization of the figure

    class Node {
      constructor(id) {
        this.id = id
        this.neighbors = new Set()}connect(node) {
        if(node ! = =this) {
          this.neighbors.add(node)
          node.neighbors.add(this)}}}class RandomGraph {
      constructor(size) {
        this.nodes = new Set(a)// Create a node
        for(let i = 0; i < size; i++) {
          this.nodes.add(new Node(i))
        }
        
        // Randomly connect nodes
        const threshold = 1 / size
        for(const x of this.nodes) {
          for(const y of this.nodes) {
            if(Math.random() < threshold) {
              x.connect(y)
            }
          }
        }
      }
      
      print() {
        for(const node of this.nodes) {
          const ids = [...node.neighbors]
                 .map(n= >n.id)
                 .join(', ');
          console.log(`${node.id}:${ids}`)}}}const g = new RandomGraph(6)
    g.print
    Copy the code
    • The graph structure is suitable for recursive traversal. Generator functions must take an iterable, produce each value in that object, and recurse on each value that can be used to test whether a graph is connected. Are there no unreachable nodes (depth-first traversal)
    class Node {
      constructor(id){... }connect(node){... }}class RandomGraph {
      constructor(size){... }print(){... }isConnected() {
        const visitedNodes = new Set(a)function* traverse(nodes){
       for(const node of nodes){
            if(! visitedNodes.has(node)){yield node;
              yield* traverse(node.neighbors)
            }
          }
        }
        
        // get the first node in the set
        const firstNode = this.nodes[Symbol.iterator]().next().value
        
        // Use recursive generators to iterate over each node
        for(const node of traverse([firstNode])){
          visitedNodes.add(node)
        }
        return visitedNodes.size === this.nodes.size
      }
    }
    Copy the code

    Generator as default iterator

class Foo {
 constructor() {
    this.value = [1.2.3[]} *Symbol.iterator]() {
    yield* this.value
  }
}

const f = new Foo()
for(const x of f){
  console.log(x)
}
Copy the code
  • The for-of loop calls the default iterator (which is also a generator function) and produces a generator object that is iterable and therefore completely usable during iteration.

4. Kill the generator early

  • The generator contains a throw() in addition to next() and return().

    function* generatorFn() {}
    const g = generatorFn();
    
    g // generatorFn {<suspended>}
    g.next // f next() { [native code] }
    g.return // f return() { [native code] }
    g.throw // f throw() { [native code] }
    Copy the code
  • Both the return() and throw() methods can be used to force the generator into a closed state.

1, the return ()

  • Forces the generator into a closed state, supplying the return() method with the value of the terminating iterator object

    function* generatorFn() {
      for(const x of [1.2.3]) {
        yieldx; }}const g = generatorFn()
    g // generatorFn {<suspended>}
    g.return(4) // {done: true, value: 4}
    g // generatorFn {<closed>}
    Copy the code
  • All generator objects have a return method that, once entered into a closed state, cannot be recovered. Subsequent calls to next() return done: true

  • Built-in languages such as for-of loops automatically ignore values returned internally from interatorObjects with a done: True state.

    function* generatorFn() {
      for(const x of [1.2.3]) {
        yieldx; }}const g = generatorFn()
    for(const x of g) {
      if(x > 1) {
        g.return(4)}console.log(x)
    }
    / / 1
    / / 2
    Copy the code

2, throw ()

  • The throw method injects a supplied error into the generator object when paused. If the error is not handled, the generator is shut down.

    function* generatorFn() {
      for(const x of [1.2.3]) {
        yield x
      }
    }
    
    const g = generatorFn()
    g // generatorFn {<suspended>}
    try{
      g.throw('foo')}catch(e) {
      console.log(e) // foo
    }
    g // generatorFn {<closed>}
    Copy the code
  • If the generator function handles the error internally, the generator is not shut down, execution can resume, and the error handling skips the corresponding yield.

    function* generatorFn() {
      for(const x of [1.2.3]) {
        try{
          yield x
        }catch(e) {}
      }
    }
    
    const g = generatorFn()
    g.next() // {done: false, value: 1}
    g.throw('foo') {done: false, vlaue: 2}
    g.next() // {done: false, value: 3}
    Copy the code
  • If the generator object has not yet started executing, the error thrown by the call to throw() will not be caught inside the function because it is equivalent to throwing an error outside the function block