Iterator 和 for...of 循环

一、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;  
      })
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容