1.现象

for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性。 for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

无论是for...in还是for...of语句都是迭代一些东西。它们之间的主要区别在于它们的迭代方式。
for...in 语句以任意顺序迭代对象的可枚举属性
for...of 语句遍历可迭代对象(实现了iterator接口)定义要迭代的数据。

这里有几个名词, 可枚举属性 可迭代对象 iterator(迭代器)

可枚举属性:

  • 可枚举属性是指那些内部 “可枚举” 标志设置为 true 的属性。对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。但是对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false。
  • 其中js中基本包装类型的原型属性是不可枚举的,如Object, Array, Number等。
  • 可枚举的属性可以通过for…in循环进行遍历(除非该属性名是一个Symbol),或者通过Object.keys()方法返回一个可枚举属性的数组。

3种遍历方法:

  • for…in 遍历对象自己的可枚举属性和原型上的可枚举属性
  • Object.keys() 只能遍历对象自己的可枚举属性,不可遍历原型可枚举属性
  • Object.getOwnPropertyNames() 遍历对象自身的所有属性,包括可枚举与不可枚举,但原型上的属性无法遍历。

可迭代对象:

实现了可迭代协议的对象。 可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))。 要成为可迭代对象, 一个对象必须实现 **@@iterator** 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:

属性
[Symbol.iterator] 一个无参数的函数,其返回值为一个符合迭代器协议的对象。

迭代器协议:

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。 只有实现了一个拥有以下语义(semantic)的 **next()** 方法,一个对象才能成为迭代器:

属性
next 一个无参数函数,返回一个应当拥有以下两个属性的对象:done(boolean)如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。value迭代器返回的任何 JavaScript 值。done 为 true 时可省略。这个值就是for..of迭代返回的值next() 方法必须返回一个对象,该对象应当有两个属性: donevalue,如果返回了一个非对象值(比如 falseundefined),则会抛出一个 TypeError 异常("iterator.next() returned a non-object value")。


备注:**

不可能判断一个特定的对象是否实现了迭代器协议,然而,创造一个同时满足迭代器协议和可迭代协议的对象是很容易的(如下面的示例中所示)。 这样做允许一个迭代器能被各种需要可迭代对象的语法所使用。因此,很少会只实现迭代器协议,而不实现可迭代协议。

  1. var myIterator = {
  2. next: function() { // 满足迭代器协议
  3. return {
  4. done: false,
  5. value: 'someValue'
  6. }
  7. },
  8. [Symbol.iterator]: function() { return this } // 满足可迭代协议
  9. }

2. 原因

为什么需要迭代器 Iterator ?

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令**for...of循环,Iterator 接口主要供for...of**消费。
详见 阮一峰es6 Iterator讲解

为什么已经有了for…in之后,还需要再设计for…of ?

for…in只能迭代对象的可枚举属性,而for…of是遍历实现了itertaor(可迭代协议)接口的成员。两者完全不同

  1. Object.prototype.objCustom = function () {};
  2. Array.prototype.arrCustom = function () {};
  3. // iterator从原型上继承了两个属性,并直接赋值了一个属性,这三个属性都是可枚举的。但是,这三个属性并没有实现iterator接口
  4. let iterable = [3, 5, 7];
  5. iterable.foo = "hello";
  6. // for...in 迭代对象的可枚举属性
  7. for (let i in iterable) {
  8. console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom"
  9. }
  10. // for...of 迭代对象实现了iterator(迭代器)接口的成员,并返回值
  11. for (let i of iterable) {
  12. console.log(i); // logs 3, 5, 7
  13. }

而且对数组,字符串使用for…in 得到的是数组和字符串的索引。
image.png
因为数组中的元素可以看做数组对象的值
字符串中的元素可以看做String对象的属性

  1. const a = ['a', 'b', 'c']
  2. a[1] = 'a'
  3. const a2 = 'abcd'
  4. a2[1] = 'a'

for…in 不可退出遍历
for…of 可以 break, continue, return 来控制遍历中的过程