一文带你理解:可以迭代大部分数据类型的 for…of 为什么不能遍历普通对象?

for…of 及其使用

  我们知道,ES6 中引入 for...of 循环,很多时候用以替代 for...inforEach() ,并支持新的迭代协议。for...of 允许你遍历 Array(数组), String(字符串), Map(映射), Set(集合),TypedArray(类型化数组)、arguments、NodeList对象、Generator等可迭代的数据结构等。for...of语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的执行语句。

for...of的语法:

for (variable of iterable) {
    // statement
}
// variable:每个迭代的属性值被分配给该变量。
// iterable:一个具有可枚举属性并且可以迭代的对象。

常用用法

{
  // 迭代字符串
  const iterable = 'ES6';
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // "E"
  // "S"
  // "6"
}
{
  // 迭代数组
  const iterable = ['a', 'b'];
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // a
  // b
}
{
  // 迭代Set(集合)
  const iterable = new Set([1, 2, 2, 1]);
  for (const value of iterable) {
    console.log(value);
  }
  // Output:
  // 1
  // 2
}
{
  // 迭代Map
  const iterable = new Map([["a", 1], ["b", 2], ["c", 3]]);
  for (const entry of iterable) {
    console.log(entry);
  }
  // Output:
  // ["a", 1]
  // ["b", 2]
  // ["c", 3]

  for (const [key, value] of iterable) {
    console.log(value);
  }
  // Output:
  // 1
  // 2
  // 3
}
{
  // 迭代Arguments Object(参数对象)
  function args() {
    for (const arg of arguments) {
      console.log(arg);
    }
  }
  args('a', 'b');
  // Output:
  // a
  // b
}
{
  // 迭代生成器
  function* foo(){ 
    yield 1; 
    yield 2; 
    yield 3; 
  }; 

  for (let o of foo()) { 
    console.log(o); 
  }
  // Output:
  // 1
  // 2
  // 3
}

Uncaught TypeError: obj is not iterable

// 普通对象
const obj = {
  foo: 'value1',
  bar: 'value2'
}
for(const item of obj){
  console.log(item)
}
// Uncaught TypeError: obj is not iterable

  可以看出,for of可以迭代大部分对象甚至字符串,却不能遍历普通对象。

如何用for...of迭代普通对象

  通过前面的基本用法,我们知道,for...of可以迭代数组、Map等数据结构,顺着这个思路,我们可以结合对象的Object.values()Object.keys()Object.entries()方法以及解构赋值的知识来用for...of遍历普通对象。

  • Object.values()Object.keys()Object.entries()用法及返回值
const obj = {
  foo: 'value1',
  bar: 'value2'
}
// 打印由value组成的数组
console.log(Object.values(obj)) // ["value1", "value2"]

// 打印由key组成的数组
console.log(Object.keys(obj)) // ["foo", "bar"]

// 打印由[key, value]组成的二维数组
// copy(Object.entries(obj))可以把输出结果直接拷贝到剪贴板,然后黏贴
console.log(Object.entries(obj)) // [["foo","value1"],["bar","value2"]]
  • 因为for...of可以迭代数组和Map,所以我们得到以下遍历普通对象的方法
const obj = {
  foo: 'value1',
  bar: 'value2'
}
// 方法一:使用for of迭代Object.entries(obj)形成的二维数组,利用解构赋值得到value
for(const [, value] of Object.entries(obj)){
  console.log(value) // value1, value2
}

// 方法二:Map
// 普通对象转Map
// Map 可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组
console.log(new Map(Object.entries(obj)))

// 遍历普通对象生成的Map
for(const [, value] of new Map(Object.entries(obj))){
  console.log(value) // value1, value2
}

// 方法三:继续使用for in
for(const key in obj){
  console.log(obj[key]) // value1, value2
}

{
  // 方法四:将【类数组(array-like)对象】转换为数组
  // 该对象需具有一个 length 属性,且其元素必须可以被索引。
  const obj = {
    length: 3, // length是必须的,否则什么也不会打印
    0: 'foo',
    1: 'bar',
    2: 'baz',
    a: 12  // 非数字属性是不会打印的
  };
  const array = Array.from(obj); // ["foo", "bar", "baz"]
  for (const value of array) { 
      console.log(value);
  }
  // Output: foo bar baz
}
{
  // 方法五:给【类数组】部署数组的[Symbol.iterator]方法【对普通字符串属性对象无效】
  const 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'
  }
}

注意事项

  • 有别于不可终止遍历的forEachfor...of的循环可由breakthrowcontinuereturn终止,在这些情况下,迭代器关闭。
  const obj = {
    foo: 'value1',
    bar: 'value2',
    baz: 'value3'
  }
  for(const [, value] of Object.entries(obj)){
    if (value === 'value2') break // 不会再执行下次迭代
    console.log(value) // value1
  };
  [1,2].forEach(item => {
      if(item == 1) break // Uncaught SyntaxError: Illegal break statement
      console.log(item)
  });
  [1,2].forEach(item => {
      if(item == 1) continue // Uncaught SyntaxError: Illegal continue statement: no surrounding iteration statement
      console.log(item)
  });
  [1,2].forEach(item => {
      if(item == 1) return // 仍然会继续执行下一次循环,打印2
      console.log(item) // 2
  })
  • For…ofFor…in对比

    • for...in 不仅枚举数组声明,它还从构造函数的原型中查找继承的非枚举属性;
    • for...of 不考虑构造函数原型上的不可枚举属性(或者说for...of语句遍历可迭代对象定义要迭代的数据。);
    • for...of 更多用于特定的集合(如数组等对象),但不是所有对象都可被for...of迭代。
      Array.prototype.newArr = () => {};
      Array.prototype.anotherNewArr = () => {};
      const array = ['foo', 'bar', 'baz'];
      for (const value in array) { 
        console.log(value); // 0 1 2 newArr anotherNewArr
      }
      for (const value of array) { 
        console.log(value); // 'foo', 'bar', 'baz'
      }
    

普通对象为何不能被 for of 迭代

  前面我们有提到一个词叫“可迭代”数据结构,当用for of迭代普通对象时,也会报一个“not iterable”的错误。实际上,任何具有 Symbol.iterator 属性的元素都是可迭代的。我们可以简单查看几个可被for of迭代的对象,看看和普通对象有何不同:

[图片上传失败...(image-869405-1607263974085)]

[图片上传失败...(image-86da24-1607263974085)]

[图片上传失败...(image-9dd87f-1607263974085)]

  可以看到,这些可被for of迭代的对象,都实现了一个Symbol(Symbol.iterator)方法,而普通对象没有这个方法。

  简单来说,for of 语句创建一个循环来迭代可迭代的对象,可迭代的对象内部实现了Symbol.iterator方法,而普通对象没有实现这一方法,所以普通对象是不可迭代的。

Iterator(遍历器)

  关于Iterator(遍历器)的概念,可以参照阮一峰大大的《ECMAScript 6 入门》——Iterator(遍历器)的概念

[图片上传失败...(image-a60031-1607263974085)]

  简单来说,ES6 为了统一集合类型数据结构的处理,增加了 iterator 接口,供 for...of 使用,简化了不同结构数据的处理。而 iterator 的遍历过程,则是类似 Generator 的方式,迭代时不断调用next方法,返回一个包含value(值)和done属性(标识是否遍历结束)的对象。

如何实现Symbol.iterator方法,使普通对象可被 for of 迭代

  依据上文的指引,我们先看看数组的Symbol.iterator接口:

const arr = [1,2,3];
const iterator = arr[Symbol.iterator]();
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: undefined, done: true}

  我们可以尝试给普通对象实现一个Symbol.iterator接口:

// 普通对象
const obj = {
  foo: 'value1',
  bar: 'value2',
  [Symbol.iterator]() {
    // 这里Object.keys不会获取到Symbol.iterator属性,原因见下文
    const keys = Object.keys(obj); 
    let index = 0;
    return {
      next: () => {
        if (index < keys.length) {
          // 迭代结果 未结束
          return {
            value: this[keys[index++]],
            done: false
          };
        } else {
          // 迭代结果 结束
          return { value: undefined, done: true };
        }
      }
    };
  }
}
for (const value of obj) {
  console.log(value); // value1 value2
};

  上面给obj实现了Symbol.iterator接口,甚至我们还可以像下面这样:

console.log([...obj]); // ["value1", "value2"]
console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))

  我们给obj对象实现了一个Symbol.iterator接口,在此,有一点需要说明的是,不用担心[Symbol.iterator]属性会被Object.keys()获取到导致遍历结果出错,因为Symbol.iterator这样的Symbol属性,需要通过Object.getOwnPropertySymbols(obj)才能获取,Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。

  有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法:

  • 扩展运算符...:这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组(毫不意外的,代码[...{}]会报错,而[...'123']会输出数组['1','2','3'])。
  • 数组和可迭代对象的解构赋值(解构是ES6提供的语法糖,其实内在是针对可迭代对象Iterator接口,通过遍历器按顺序获取对应的值进行赋值。而普通对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。);
  • yield*_yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口;
  • 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用;
  • 字符串是一个类似数组的对象,也原生具有Iterator接口,所以也可被for of迭代。

迭代器模式

  迭代器模式提供了一种方法顺序访问一个聚合对象中的各个元素,而又无需暴露该对象的内部实现,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。迭代器模式为遍历不同的集合结构提供了一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。

  不难发现,Symbol.iterator实现的就是一种迭代器模式。集合对象内部实现了Symbol.iterator接口,供外部调用,而我们无需过多的关注集合对象内部的结构,需要处理集合对象内部的数据时,我们通过for of调用Symbol.iterator接口即可。

  比如针对前文普通对象的Symbol.iterator接口实现一节的代码,如果我们对obj里面的数据结构进行了如下调整,那么,我们只需对应的修改供外部迭代使用的Symbol.iterator接口,即可不影响外部迭代调用:

const obj = {
  // 数据结构调整
  data: ['value1', 'value2'],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          // 迭代结果 未结束
          return {
            value: this.data[index++],
            done: false
          };
        } else {
          // 迭代结果 结束
          return { value: undefined, done: true };
        }
      }
    };
  }
}
// 外部调用
for (const value of obj) {
  console.log(value); // value1 value2
}

  实际使用时,我们可以把上面的Symbol.iterator提出来进行单独封装,这样就可以对一类数据结构进行迭代操作了。当然,下面的代码只是最简单的示例,你可以在此基础上探究更多实用的技巧。

const obj1 = {
  data: ['value1', 'value2']
}
const obj2 = {
  data: [1, 2]
}
// 遍历方法
consoleEachData = (obj) => {
  obj[Symbol.iterator] = () => {
    let index = 0;
    return {
      next: () => {
        if (index < obj.data.length) {
          return {
            value: obj.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
  for (const value of obj) {
    console.log(value);
  }
}
consoleEachData(obj1); // value1 value2
consoleEachData(obj2); // 1  2

一点补充

  在写这篇文章时,有个问题给我带来了困扰:原生object对象默认没有部署Iterator接口,即object不是一个可迭代对象,那么,原生object对象的解构赋值是怎么样一种机制呢?
  有一种说法是:ES6提供了Map数据结构,实际上原生object对象被解构时,会被当作Map进行解构。关于这点,大家有什么不同的观点吗?欢迎评论区一起探讨。

参考资料

本文首发于个人博客,欢迎指正和star

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

推荐阅读更多精彩内容