for-of循环对象

今天,某神问了我一个for-of循环对象(我说的普通对象,不是类似数组的对象)问题,我的回答是为对象部署Iterator 接口,回答是正确的,但是如何部署Iterator 接口,回答得有点说得模糊,而且方法也不只这一种。下面我为这个问题,亲自做个总结。

Iterator 接口

回答这个问题,我们先了解下 Iterator 接口,Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for…of循环。当使用for…of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};

下面是原生具备 Iterator 接口的数据结构:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

其中object是没有这个Iterator 接口的。

方法一:部署Iterator 接口(在Symbol.iterator的属性上部署遍历器生成方法)

一个对象如果要具备可被for…of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}

[Symbol.iterator]() { return this; }

next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}

function range(start, stop) {
return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}

方法二:使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组

1
2
3
for (var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}

方法三:使用 Generator 函数将对象重新包装一下

1
2
3
4
5
6
7
8
9
10
11
12
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}

for (let [key, value] of entries(obj)) {
console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

方法四:使用 Map 数据结构代替

对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。