乐趣区

关于前端:一文带你理解可以迭代大部分数据类型的-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 迭代的对象,看看和一般对象有何不同:

  能够看到,这些可被 for of 迭代的对象,都实现了一个 Symbol(Symbol.iterator) 办法,而一般对象没有这个办法。

  简略来说,for of 语句创立一个循环来迭代可迭代的对象,可迭代的对象外部实现了 Symbol.iterator 办法,而一般对象没有实现这一办法,所以一般对象是不可迭代的。

Iterator(遍历器)

  对于 Iterator(遍历器)的概念,能够参照阮一峰大大的《ECMAScript 6 入门》——Iterator(遍历器)的概念:

  简略来说,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 进行解构。对于这点,大家有什么不同的观点吗?欢送评论区一起探讨。

参考资料

  • MDN:for…of
  • Understanding the JavaScript For…of Loop(【译文】)
  • Iterator 和 for…of 循环

本文首发于集体博客,欢送斧正和 star。

退出移动版