3

Iterator

Iterator concept

Iterator provides a unified interface mechanism to provide a unified access mechanism for various data structures. Defining an Iterator is to provide an object with a next() method. Each call to next() will return a result object. The result object has two properties, value indicates the current value, and done indicates whether the traversal is over.

 function makeIterator(Array){
    let index = 0;
    return {
        next: function(){
            return (
                Array.length > index ?
                {value: Array[index++]}:
                {done: true}
            )
        }
    }
}

let iterator = makeIterator(['1','2'])
console.log(iterator.next()); // {value: '1'}
console.log(iterator.next()); // {value: '2'}
console.log(iterator.next()); // {done: true}

The role of Iterator:

  1. Provide a unified and convenient access interface for various data structures;
  2. Enables the members of the data structure to be arranged in a certain order;
  3. for...of consumption

Default Iterator interface

ES6 provides the for of statement to traverse the iterator object. Let's use the for of statement to traverse the iterator created above:

 let iterator = makeIterator(['1','2'])
for (let value of iterator) {
    console.log(value);
} // iterator is not iterable

The result is an error saying iterator is not iterable, why is this?
ES6 stipulates that the default Iterator interface is deployed in the Symbol.iterator property of the data structure. If a data structure has the Symbol.iterator property, the data structure can be traversed. We transform our custom makeIterator as follows:

 const MakeIterator = (Array) => ({
    [Symbol.iterator](){
        let index = 0;
        return {
            next(){
                let length = Array.length;
                if(index < length){
                    return {value: Array[index++]}
                }else{
                    return {done: true}
                }
            }
        }
    }
})
for(let value of MakeIterator([1,2])){
    console.log(value)
}
// 1
// 2

return() of Iterator

We add a return method to MakeIterator, and if the for...of loop exits early (usually because of an error, or a break statement), the return() method is called to terminate the traversal. Based on this feature, if an object needs to clean up or release resources before completing the traversal, we can deploy the return() method to close the file when the file reading fails.

 const MakeIterator = (Array) => ({
    [Symbol.iterator](){
        let index = 0;
        return {
            next(){
                let length = Array.length;
                if(index < length){
                    return {value: Array[index++]}
                }else{
                    return {done: true}
                }
            },
            return(){
                return {done: true}
            }
        }
    }
})
for(let value of MakeIterator([1, 2, 3])){
    console.log(value) // 1
    // 方式1
    break;
    // 方式2
    // throw new Error('error');
}

Native data structure with Iterator interface

array
Set
Map
Array-like objects, such as arguments object, DOM NodeList object, typedArray object

 // arguments 对象
function sum(){
    for(let value of arguments){
        console.log(value)
    }
}
sum(1,2)
// 1
// 2

// typedArray 对象
let typeArry = new Int8Array(2);
typeArry[0] = 1;
typeArry[1] = 2;
for(let value of typeArry){
    console.log(value) 
}
// 1
// 2
  1. Generator object

     function* gen(){
     yield 1;
     yield 2;
    }
    for(let value of gen()){
     console.log(value)
    }
  2. String

Q: Why doesn't Object have a native Iterator?

A: The reason why the object (Object) does not deploy the Iterator interface by default is because it is uncertain which property of the object is traversed first and which property is traversed later. Essentially, a traverser is a linear process. For any nonlinear data structure, deploying a traverser interface is equivalent to deploying a linear transformation. However, strictly speaking, the object deployment traverser interface is not very necessary, because then the object is actually used as a Map structure, ES5 does not have a Map structure, and ES6 provides it natively.

When calling the Iterator interface

  1. destructuring assignment

     let set = new Set().add('a').add('b').add('c');
    
    let [x,y] = set; // x='a'; y='b'
  2. spread operator

     var str = 'hello';
    [...str] //  ['h','e','l','l','o']

    The extension operator is to call the Iterator interface, so Object does not deploy the Iterator interface, so why can the ... operator be used?
    Reason: There are two types of spread operators

  • One is used in the case of function parameters and array expansion, which requires the object to be iterable (iterable)
  • The other is for object expansion, that is, in the form of {…obj}, in which case the object is required to be enumerable (enumerable)

     let obj1 = {
      name: 'qianxun'
    } 
    let obj2 = {
      age: 3
    }
    // 数组对象是可枚举的
    let obj = {...obj1, ...obj2}
    console.log(obj) //{name: 'qianxun', age: 3}
    
    // 普通对象默认是不可迭代的
    let obj = [...obj1, ...obj2]
    console.log(obj) // object is not iterable

    Mock implementation for of

     function forOf(obj, cb){
      let iteratorValue = obj[Symbol.iterator]();
      let result = iteratorValue.next()
      while(!result.done){
          cb(result.value)
          result = iteratorValue.next()
      }
    }
    
    forOf([1,2,3], (value)=>{
      console.log(value)
    })
    // 1
    // 2
    // 3

    Generator

    Meet the Generator

     // 概念上
    Generator 函数是 ES6 提供的一种异步编程解决方案。Generator 函数是一个状态机,封装了多个内部状
    态;Generator 函数还是一个遍历器对象生成函数,执行后返回一个遍历器对象。
    
    // 形式上
    1.function 关键字与函数名之间有一个星号;
    2.函数体内部使用 yield 表达式,定义不同的内部状态。
     function* simpleGenerator(){
      yield 1;
      yield 2;
    }
    simpleGenerator()

    As above, we created a simple Generator, and we explored it with two questions:

  1. What happens after the Generator function runs?
  2. What does the yield expression in a function do?

     function* simpleGenerator(){
     console.log('hello world');
     yield 1;
     yield 2;
    }
    let generator = simpleGenerator(); // simpleGenerator {<suspended}}
    console.log(generator.next())
    // hello world
    // {value: 1, done: false}
    console.log(generator.next())
    // {value: 2, done: false}

    Generator The generator function returns a generator object after running, and the ordinary function will directly execute the code inside the function; each time the next method of the generator object is called, the function will be executed until the next yield keyword stops execution, and returns a {value: Value, done: Boolean} object.

    Parameters of the next method

    The yield expression itself does not return a value, or always returns undefined. The next method can take one parameter, which will be used as the return value of the previous yield expression. Through the parameters of the next method, different values can be injected from the outside to the inside at different stages of the Generator function's operation, thereby adjusting the function's behavior.
    Since the parameter of the next method represents the return value of the previous yield expression, it is invalid to pass the parameter when the next method is used for the first time.

     function sum(x){
     return function(y){
         return x + y;
     }
    }
    console.log(sum(1)(2))
    
    // 利用next传参改写
    function* sum(x){
     let y = yield x;
     while(true){
        y = yield x + y;
     }
    }
    
    let gen = sum(2)
    console.log(gen.next()) // 2
    console.log(gen.next(1)) // 3
    console.log(gen.next(2))  // 4

    yield expression

    The role of the yield expression: define internal state and pause execution
    Difference between yield expression and return statement

  • The yield expression indicates that the function suspends execution and continues to execute backwards from this position next time, and the return statement does not have the function of position memory
  • In a function, only one return statement can be executed, but multiple yield expressions can be executed
  • Any function can use the return statement, and the yield expression can only be used in the Generator function, and an error will be reported in other places
  • If the yield expression participates in the operation, it is placed in parentheses; when it is used as a function parameter or placed on the right side of an assignment expression, parentheses can be omitted.

     function *gen () {
    console.log('hello' + yield) ×
    console.log('hello' + (yield)) √
    console.log('hello' + yield 1) ×
    console.log('hello' + (yield 1)) √
    foo(yield 1)  √
    const param = yield 2  √
    }

    Based on the generator function that supports multiple yields, we can implement a scenario where a function has multiple return values:

     function* gen(num1, num2){
      yield num1 + num2;
      yield num1 - num2;
    }
    
    let res = gen(2, 1);
    console.log(res.next()) // {value: 3, done: false}
    console.log(res.next()) // {value: 1, done: false}

    The relationship between Generator and Iterator

    Since the Generator function is the traverser generating function, the Generator can be assigned to the Symbol.iterator property of the object, so that the object has the Iterator interface. Generator implementation code is more concise.

     let obj = {
      name: 'qianxun',
      age: 3,
      [Symbol.iterator]: function(){
          let that = this;
          let keys = Object.keys(that)
          let index = 0;
          return {
              next: function(){
                  return index < keys.length ?
                  {value: that[keys[index++]], done: false}:
                  {value: undefined, done: true}
              }
          }
      }
    }
    for(let value of obj){
      console.log(value)
    }

    Generator:

     let obj = {
      name: 'qianxun',
      age: 3,
      [Symbol.iterator]: function* (){
          let keys = Object.keys(this)
          for(let i=0; i< keys.length; i++){
              yield this[keys[i]];
          }
      }
    }
    for(let value of obj){
      console.log(value)
    }

    Generator.prototype.return()

    return() method, which can return the given value and terminate the traversal of the Generator function.
 function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
// 如果 return() 方法调用时,不提供参数,则返回值的 value 属性为 undefined
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

If there is a try...finally code block inside the Generator function, and the ---87ce55e09e25e9dbf3b599ef953e4daa try code block is being executed, then the return() method will cause immediate entry into the finally code block ---cb5a0d53301dd9 , after the execution, the whole function will end.

 function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

yield* expressions

If you want to be inside a Generator function, call another Generator function. We need to manually complete the traversal inside the function body of the former. If the function calls are nested in multiple layers, the writing method will be cumbersome and difficult to read. ES6 provides the yield* expression as a solution.

Delegate to other generators

 function* g1() {
  yield 2;
  yield 3;
}

function* g2() {
  yield 1;
  yield* g1();
  yield 4;
}

const iterator = g2();

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

Delegate to other iterables

 function* gen(){
    yield* [1,2,3]
}
console.log(gen().next()) // {value: 1, done: false}

the this of the Generator function

The Generator function returns a traverser. ES6 specifies that this traverser is an instance of the Generator function, inheriting the methods on the Generator.prototype object, but cannot get the properties on this, because this is the global object, not the instance object.

 function* gen(){
    this.a = 1
}
gen.prototype.say = function(){
    console.log('hi')
}
let obj = gen()
console.log(obj instanceof gen) // true
obj.say() // hi
obj.next()
console.log(obj.a) //undefined

If you want to access instance properties like a constructor, you can modify this to bind to Generator.prototype.

 function* gen(){
    this.a = 1
}
gen.prototype.say = function(){
    console.log('hi')
}   
let obj = gen.call(gen.prototype)
console.log(obj instanceof gen) // true
obj.say() // hi
obj.next()
console.log(obj.a) //1

Generator implements a state machine

 function* StateMachine(state){
    let transition;
    while(true){
        if(transition === "INCREMENT"){
            state++;
        }else if(transition === "DECREMENT"){
            state--;
        }
        transition = yield state;
    }
}
const iterator = StateMachine(0);
console.log(iterator.next()); // 0
console.log(iterator.next('INCREMENT')); // 1
console.log(iterator.next('DECREMENT')); // 0

袋鼠云数栈UED
277 声望34 粉丝

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。