美文网首页
Iterator 和 for...of 循环

Iterator 和 for...of 循环

作者: magic_pill | 来源:发表于2019-06-10 20:24 被阅读0次

    一、Iterator(遍历器)的概念

    • Iterator 是一种接口,为各种不同的数据结构提供统一的访问机制。
    • 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
    • 表示“集合”的数据结构:数组(Array)、对象(Object)、Map 和 Set。开发者还可以组合使用它们,定义自己的数据结构,比如数组的成员是 Map,Map 的成员是对象。
    Iterator 的作用:
    • 为各种数据结构提供一个统一的、简便的访问接口;
    • 使得数据结构的成员能够按某种次序排序;
    • ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。
    Iterator 的遍历过程:
    • 创建一个指针对象,指向当前数据结构的起始位置。遍历器对象本质上,就是一个指针对象。
    • 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
    • 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
    • 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++], done: false} :
            {value: undefined, done: true};
        }
      };
    }
    var it = makeIterator(['a', 'b']);
    console.log(it.next())  // { value: "a", done: false }
    console.log(it.next())  // { value: "b", done: false }
    console.log(it.next())  // { value: undefined, done: true }
    

    说明:定义了一个 makeIterator 遍历器生成函数,它返回一个遍历器对象。对数组执行这个函数,返回数组的遍历器对象 it

    • 其中,done:falsevalue:undefined 可以省略,上面的 makeIterator 方法可以改写如下:
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++]} :
            {done: true};
        }
      };
    }
    

    注:Iterator 只是把接口规格加到数据结构之上,遍历器与它所遍历的那个数据结构,实际上是分开的;完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。

    var it = sqrMaker();
    
    console.log(it.next().value) // 0
    console.log(it.next().value) // 1
    console.log(it.next().value) // 4
    console.log(it.next().value) // 9
    // ...
    
    function sqrMaker() {
      var index = 0;
    
      return {
        next: function() {
          return {value: index++ ** 2, done: false};
        }
      };
    }
    

    二、默认 Iterator 接口

    • ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
    • Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值。
    • 有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被 for...of 循环遍历,因为这些数据结构原生部署了 Symbol.iterator 属性。
      • 原生具备 Iterator 接口的数据结构如下:

        1、Array
        2、Map
        3、Set
        4、String
        5、TypedArray
        6、函数的 arguments 对象
        7、NodeList 对象

      • 举例,数组的 Symbol.iterator 属性:
            var arr = ['a','b','c','d'];
            var arrayIt = arr[Symbol.iterator]();
            console.log(arrayIt.next());  //{value: "a", done: false}
            console.log(arrayIt.next());  //{value: "b", done: false}
            console.log(arrayIt.next());  //{value: "c", done: false}
            console.log(arrayIt.next());  //{value: "d", done: false}
            console.log(arrayIt.next());  //{value: undefined, done: true}
        
      • 说明:

        1、变量 array 是一个数组,原生就具有遍历器接口,部署在 array 的 Symbol.iterator 属性上面。所以,调用这个属性,就得到遍历器对象。
        2、对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

    一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可):
      class RangeIterator {
        constructor(start, stop) {
          this.value = start;
          this.stop = stop;
        }
    
        [Symbol.iterator]() { return this; }
    
        next() {
          var value = this.value;
          if (value < this.stop) {
            this.value++;
            return {done: false, value: value};
          }
          return {done: true, value: undefined};
        }
      }
    
      function range(start, stop) {
        return new RangeIterator(start, stop);
      }
    
      for (var value of range(0, 3)) {
        console.log(value); // 0, 1, 2
      }
    
    • 说明:上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator
      属性对应一个函数,执行后返回当前对象的遍历器对象。
    为对象添加 Iterator 接口的例子:
        var objt = {
          data: [ 'hello', 'world' ],
          [Symbol.iterator]() {
            const self = this;
            let index = 0;
            return {
              next() {
                if (index < self.data.length) {
                  return {
                    value: self.data[index++],
                    done: false
                  };
                } else {
                  return { value: undefined, done: true };
                }
              }
            };
          }
        };
        var o = objt[Symbol.iterator]()
        console.log(o.next())   // {value: "hello", done: false}
        console.log(o.next())   // {value: "world", done: false}
        console.log(o.next())   // {value: undefined, done: true}
    
    类数组对象:
    • 存在数值键名和 length 属性;
    • 部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。
        NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
        // 或者
        NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
      
        [...document.querySelectorAll('div')] // 可以执行了
      
      • 说明:NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的Symbol.iterator属性,可以看到没有任何影响。
    类似数组的对象调用数组的 Symbol.iterator 方法的例子:
        let iterable = {
          0: 'a',
          1: 'b',
          2: 'c',
          length: 3,
          [Symbol.iterator]: Array.prototype[Symbol.iterator]
        };
        for (let item of iterable) {
          console.log(item); // 'a', 'b', 'c'
        }
    
    • 普通对象部署数组的Symbol.iterator方法,并无效果:
        let iterable2 = {
          a: 'a',
          b: 'b',
          c: 'c',
          length: 3,
          [Symbol.iterator]: Array.prototype[Symbol.iterator]
        };
        for (let item of iterable2) {
          console.log(item); // undefined, undefined, undefined
        }
      
    • 如果 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错:
        var obj2 = {};
        obj2[Symbol.iterator] = () => 1;
        [...obj2] 
        // 编辑器 ----> TypeError: obj2[Symbol.iterator] is not a function
        // 浏览器 ----> TypeError: Result of the Symbol.iterator method is not an object
      
    • 有了遍历器接口,数据结构就可以用 for...of 循环遍历,也可以使用 while 循环遍历:
        var $iterator = ['x', 'y', 'z'][Symbol.iterator]();
        var $result = $iterator.next();
        while (!$result.done) {
          var x = $result.value;
          console.log('yjw ------- ', x);
          $result = $iterator.next();
        }
      

    三、调用 Iterator 接口的场景

    解构赋值:
    • 对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator 方法:
      var set = new Set().add('a').add('b').add('c');
      var [x,y] = set;
      console.log('x = ', x, ' ; y = ', y);   
      // x =  a  ; y =  b
      var [first, ...rest] = set;
      console.log('first = ', first, ' ; rest = ', rest);   
      // first =  a  ; rest =  [ 'b', 'c' ]
      
      var arr = ['b', 'd', 'e'];
      Array.prototype[Symbol.iterator] = function(){
          const self = this;
          let index = 0;
          return {
            next() {
              if (index < self.length) {
                  console.log('yjw ----------> ', index)
                return {
                  value: self[index++],
                  done: false
                };
              } else {
                return { value: undefined, done: true };
              }
            }
          };
      }
      var [n1,n2] = arr
      // yjw ---------->  0
      // yjw ---------->  1
      
    扩展运算符:
      // 例一
      var str = 'hello';
      [...str] //  ['h','e','l','l','o']
    
      // 例二
      let arr = ['b', 'c'];
      ['a', ...arr, 'd']
      // ['a', 'b', 'c', 'd']
    

    补充:只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

    yield *
    • yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口
        var generator = function* () {
          yield 1;
          yield* [2,3,4];
          yield 5;
        };
      
        var iterator = generator();
      
        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: 5, done: false }
        console.log(iterator.next()) // { value: undefined, done: true }
      
    • 其它场合:
      • 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口:

      for...of
      Array.from()
      Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
      Promise.all()
      Promise.race()

    四、字符串的 Iterator 接口

    • 字符串是一个类似数组的对象,原生具有 Iterator 接口
        var someString = "hi";
        console.log(typeof someString[Symbol.iterator])
        // "function"
      
        var iterator = someString[Symbol.iterator]();
        console.log(iterator.next())  // { value: "h", done: false }
        console.log(iterator.next())  // { value: "i", done: false }
        console.log(iterator.next())  // { value: undefined, done: true }
      
    • 覆盖字符串原生的 Symbol.iterator 方法,修改遍历器行为
        var str = new String("hi");
      
        console.log([...str]) // ["h", "i"]
      
        str[Symbol.iterator] = function() {
          return {
            next: function() {
              if (this._first) {
                this._first = false;
                console.log('============= yjw ============')
                return { value: "bye", done: false };
              } else {
                return { done: true };
              }
            },
            _first: true
          };
        };
      
        console.log([...str]) // ["bye"]
        console.log(str) // "hi"
      

    五、Iterator 接口与 Generator 函数

    • Symbol.iterator方法的最简单实现:
        var myIterable2 = {
          [Symbol.iterator]: function* () {
            yield 1;
            yield 2;
            yield 3;
          }
        };
        console.log([...myIterable2]) // [1, 2, 3]
      
        // 或者采用下面的简洁写法
      
        var obj2 = {
          * [Symbol.iterator]() {
            yield 'hello';
            yield 'world';
          }
        };
      
        for (let x of obj2) {
          console.log(x);
        }
        // "hello"
        // "world"
      
    遍历器对象构成条件:
    • next:必须要部署该方法;
    • return:可选的部署方法;
    • throw:主要配合 generator(下一章) 函数使用,一般的遍历对象用不到这个方法。

    六、for...of 循环

    • 一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员;

    • 使用范围:数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

        var testArr = ['x', 'y', 'z'];
        for (let v of testArr){
            console.log(v)  // x  y   z
        }
      
        var obj3 = {};
        obj3[Symbol.iterator] = testArr[Symbol.iterator].bind(testArr);
      
        for(let v of obj3) {
          console.log(v); // x  y   z
        }
      
    • 可以替代 forEach 方法;

    • for...in 缺点:

      • 只能获取键名,不能获取键值;
      • 数组的键名是数字,但是这里的键名是字符串;
      • 可以返回非数值键名;
      • 优点:可以遍历普通对象。
      • 结论:for...in 主要是为遍历对象设计的,不适用于遍历数组。
        var testArr = ['x', 'y', 'z'];
        testArr.foo = 'abc'
      
        for (let v of testArr){
            console.log('of ---> ', v)
        }
        
        for (let v in testArr){
            console.log('in ---> ', v)
        }
      
    • 能正确识别 32 为 UTF-16 字符:

        for (let x of 'a\uD83D\uDC0A') {
          console.log(x);
        }
        // 'a'
        // '\uD83D\uDC0A'
      
    • 有些类数组的对象没有 Iterator 接口,可以使用 Array.from 方法将其转为数组:

        var arrayLike = { 0: 'a', 1: 'b', length: 2};
      
        // 报错
        for (let x of arrayLike) {
          console.log(x);
        }
      
        // 正确
        for (let x of Array.from(arrayLike)) {
          console.log(x);
        }
        // a  b
      
    • 不能遍历普通的对象,必须部署了 Iterator 接口后才能使用。其它的解决方法:

      // 方案一:
      var someObject = {a: 'aa', b: 'bb'};
      for (var key of Object.keys(someObject)) {
        console.log(key + ': ' + someObject[key]);
      }
      // a: aa
      // b: bb
      
      // 方案二:
      function* entries(obj) {
        for (let key of Object.keys(obj)) {
          yield [key, obj[key]];
        }
      }
      
      var obj = {name: 'yijiang', age: '18'};
      for (let [key, value] of entries(obj)) {
        console.log(key, '->', value);
      }
      // name -> yijiang
      // age -> 18
      
    和其它遍历语法的比较:
    • 原始的 for 循环:麻烦;
        var array = [1,2,3,4,5,6,7,8,9]
        for(let i=0; i < array.length; i++){
            console.log(array[i]);
            if(i>5) break;
        }
        // 1 2 3 4 5 6 7
      
    • forEach 方法:无法中途跳出
        var array = [1,2,3,4,5,6,7,8,9]
        array.forEach(v => {
          console.log(v)
          if(v > 5) return;  
        })
      

    相关文章

      网友评论

          本文标题:Iterator 和 for...of 循环

          本文链接:https://www.haomeiwen.com/subject/wsstfctx.html