什么是可迭代对象(Iterable objects)?

8,885 阅读4分钟

by @zhangbao(zhangbao) #0104

可以将可迭代对象理解为“宽泛意义上的数组”——就是说,不一定是数组(Array.isArray(iterable) 返回 false),但却能够被 for...of 循环遍历。

概览

  • 可迭代对象不一定是数组,数组一定是可迭代对象。
  • 每个可迭代对象必然包含一个 [Symbol.iterator]  方法属性
  • 字符串也是可迭代对象

image.png

改造普通对象

我们举一个例子,下面有一个对象:

let range = {
  from: 1,
  to: 5
};

// 我们想用 for..of 遍历 range,得到从 1(from) 到 5(to) 的自然数
// for(let num of range) { consol.log(num) } // 遍历结果 1 -> 2 -> 3 -> 4 -> 5

明眼人一看,就知道 range 不就是个普通对象嘛,跟可迭代对象有什么关系呢?还要用 for..of 遍历,遍历结果还要是 1 -> 2 -> 3 -> 4 -> 5,这不扯淡呢吗?说的对,现在肯定是扯淡,那是因为我们啥都没做呢,可不是扯淡吗?

为了能让 range 这个普通对象变为可迭代对象,我们需要先来了解下 Symbol.iterator 这个系统预置变量。

Symbol.iterator

我们先来看下,这个属性的描述是怎样的。

Object.getOwnPropertyDescriptor(Symbol, 'iterator')
// {
//   value: Symbol(Symbol.iterator),
//   writable: false,
//   enumerable: false,
//   configurable: false
// }

由结果可知,这个属性我们是修改不了的。

下面我们为上面的 range 对象添加一个 [Symbol.iterator]  方法属性——对的,这个属性的属性值是函数,每个可迭代对象都有一个 [Symbol.iterator]  方法属性,没有的话,肯定不是可迭代对象

let range = {
  from: 1,
  to: 5
}

// 1. for..of 循环首先会调用对象上的 [Symbol.iterator] 属性——range[Symbol.iterator](),
// 属性 range[Symbol.iterator] 称为“迭代对象生成器”或“迭代对象生成函数”
range[Symbol.iterator] = function() {
	
  // range[Symbol.iterator]() 的调用结果,会返回一个包含 next 方法的对象,
  // 这个对象称为“迭代对象”
  // 2. 接下来, for..of 就是完全在跟这个迭代对象打交道了
  return {
    current: this.from,
    last: this.to,
    
    // 3. 每次 for..of 循环一次,就要调用一次 next 方法
    next() {
      // 4. 从 next 方法返回的对象中,我们能获得当前遍历的值(value)以及遍历是否结束的标记(done)
      if (this.current <= this.last) {
        return { done: false, value: this.current++ }
      } else {
        return { done: true }
      }
    }
  }
}

for...of 循环遍历的本质是:

  1. for..of 循环首先会调用对象上的方法属性 [Symbol.iterator]——range[Symbol.iterator](),得到一个包含 next 方法的对象。
  • 这个包含 next 方法的对象称为迭代对象(iterator object)
  • 属性 range[Symbol.iterator] 被称为迭代对象生成器迭代对象生成函数
  1. 接下来, for..of 就是完全在跟这个迭代对象打交道了,
  2. 每次 for..of 循环一次,就要调用一次 next 方法,
  3. next 方法返回的对象中({ done: ..., value: ... }),我们能获得当前遍历的值(value)以及遍历是否结束的标记(done)。

经过上面的叙述,我们还可以将可迭代对象定义为:能够生成“迭代对象”的对象。
**

遍历改造对象

现在对改造后的 range 对象进行遍历。

let range = {
  from: 1,
  to: 5
}
range[Symbol.iterator] = function() {...}

for (let num of range) {
  console.log(num) // 1, 然后是 2, 3, 4, 5
}

很酷啊,现在可以一次遍历出 1 -> 2 -> 3 -> 4 -> 5 这 5 个数字了。

手动遍历可迭代对象

因为好玩,咱们模仿 for...of 循环内部执行流程,纯手工写一下遍历可迭代对象的逻辑吧。

这里会用到 while 循环:

// 下面的写法,等同于
// for (let num of range) { console.log(num) };

let iterator = range[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  // 标记结束(done 为 true),就终止循环,结束遍历
  if (result.done) break;
  // 否则,打印当前遍历的值
  console.log(result.value);
}
  1. 首先,手动调用 range[Symbol.iterator] 方法,得迭代对象
  2. while 循环内部:
  3. 如果标记结束(donetrue),就终止循环,结束遍历
  4. 否则,打印当前遍历的值

内置可迭代对象

前面我们说过:可迭代对象不一定是数组,现在再加一句:数组一定是可迭代对象。根据经验,我们知道数组是可以用 for...of 循环遍历的。

数组

image.png

遍历 symbolsfor...of 循环成功遍历了。这是意料之中的事情,但我们再来看下,这个数组对象里是不是有个叫 Symbol.iterator 的属性 🕵️‍♂️

image.png

果然!

字符串

字符串也是可迭代对象。证据如下:

image.png

再来找找 Symbol.iterator 属性。

image.png

Map 和 Set

Map 和 Set 也是能够被 for...of 遍历的。在这里就不多举例了,直接展示它们各自部署的  Symbol.iterator 属性。

image.png
image.png

从上面的截图里,我们可以总结出一点内容来:

  1. Map 对象默认的迭代对象生成器函数是 map.entries(),而
  2. Set 对象默认的迭代对象生成器函数是 map.values()

更多关于 Map 和 Set 对象的遍历内容,请参考《遍历 Map 和 Set》

(完)