by @zhangbao(zhangbao) #0104
可以将可迭代对象理解为“宽泛意义上的数组”——就是说,不一定是数组(Array.isArray(iterable)
返回 false
),但却能够被 for...of
循环遍历。
概览
- 可迭代对象不一定是数组,数组一定是可迭代对象。
- 每个可迭代对象必然包含一个
[Symbol.iterator]
方法属性 - 字符串也是可迭代对象
改造普通对象
我们举一个例子,下面有一个对象:
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
循环遍历的本质是:
for..of
循环首先会调用对象上的方法属性[Symbol.iterator]
——range[Symbol.iterator]()
,得到一个包含next
方法的对象。
- 这个包含
next
方法的对象称为迭代对象(iterator object) - 属性
range[Symbol.iterator]
被称为迭代对象生成器或迭代对象生成函数
- 接下来,
for..of
就是完全在跟这个迭代对象打交道了, - 每次
for..of
循环一次,就要调用一次next
方法, - 从
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);
}
- 首先,手动调用
range[Symbol.iterator]
方法,得迭代对象 - 在
while
循环内部: - 如果标记结束(
done
为true
),就终止循环,结束遍历 - 否则,打印当前遍历的值
内置可迭代对象
前面我们说过:可迭代对象不一定是数组,现在再加一句:数组一定是可迭代对象。根据经验,我们知道数组是可以用 for...of
循环遍历的。
数组
遍历 symbols
被 for...of
循环成功遍历了。这是意料之中的事情,但我们再来看下,这个数组对象里是不是有个叫 Symbol.iterator
的属性 🕵️♂️
果然!
字符串
字符串也是可迭代对象。证据如下:
再来找找 Symbol.iterator
属性。
Map 和 Set
Map 和 Set 也是能够被 for...of
遍历的。在这里就不多举例了,直接展示它们各自部署的 Symbol.iterator
属性。
从上面的截图里,我们可以总结出一点内容来:
- Map 对象默认的迭代对象生成器函数是
map.entries()
,而- Set 对象默认的迭代对象生成器函数是
map.values()
更多关于 Map 和 Set 对象的遍历内容,请参考《遍历 Map 和 Set》。
(完)