文章目录
for..in
和 for..of
的区别,什么是迭代器?
for..in
和 for..of
的区别
得出结论的过程
我们常常使用 for..in
来遍历对象,使用 for..of
来遍历数组,如下:
let obj = {
name: 'weilangpu',
age: 19,
hight: 183
}
for(const i in obj) {
console.log(obj[i]);
}
// weilangpu
// 19
// 183
let arr = [1, 2, 3];
for(const i in arr) {
console.log(arr[i]);
}
// 1
// 2
// 3
for(const i of arr) {
console.log(i);
}
// 1
// 2
// 3
我们从上面这个例子可以看到使用 for..in
遍历 对象 或 数组 时,实际上遍历的是对象中可枚举的 属性名 或 数组的下标 ,仍然需要手动获取 值。
与之相反,使用 for..of
遍历 数组 时,实际上就是在遍历数组的 值,而非 下标
那么可不可以用 for..of
遍历对象呢?这样是不是就可以免于在循环体中手动获取 值 ?
答案是否定的。不可以!!! 编译器会报错!!! 如下:
for(const i of obj) {
console.log(i);
}
// TypeError: obj is not iterable
在报错信息中我们看到一个单词 iterable(即:可迭代对象,稍后会做介绍),编译器说 obj
不是一个 iterable 。那么反过来想,for..of
能够遍历的数组就是一个 iterable 了。
铺垫了这么久终于引出来了 iterable 这个词,那么现在可以说两者的本质区别了。
两者本质区别
for..in
和 for..of
的 本质区别 其实是:
-
for..in
用于枚举对象中的可枚举属性名。 -
for..of
用于遍历 可迭代对象 (即前文的:iterable)的元素。
iterable(可迭代对象)
在介绍这个名词前先理解一下 迭代 这个名词:
- 迭代:按照顺序反复多次执行一段程序,通常会有明确的终止条件。
简单的说就是 循环 的意思。
那么 iterable 从字面意思理解就是 可以被循环执行的对象。
有人要问了,那为什么上个例子中的 obj
不是 iterable ,它不是也可以被循环执行吗?
- 注意:不应该通过一个对象是否能被遍历而确定它是否是 iterable,即使它能够被
for
和for..in
遍历,那在程序内部中的其他方法也未必就会把它当作一个 iterable 。
那么该怎么区分 iterable 和 非iterbale 两者呢?
- 区别:是否实现了 iterable 接口。
iterable接口
实现 iterable接口 的对象,就 一定 会暴露了一个属性作为 默认的 iterator(即:迭代器,稍后会做介绍),且这个属性必须使用特殊的 Symbol.iterator
作为 键。
这个属性的值必须是一个 迭代器工厂函数 ,调用这个工厂函数必须返回一个新的 迭代器。
可以输出这个工厂函数,看看检查对象是否实现了 iterable接口 如下:
console.log(obj[Symbol.iterator]); // undefine
console.log(arr[Symbol.iterator]); // ƒ values() { [native code] }
现在可以看到前面的 obj
并没有实现 iterable接口,与之相对的 arr
实现了 iterable接口,在结合 for..of
的特性,所以 for..of
才无法遍历 obj
而可以遍历 arr
。
接下来我们介绍一下调用工厂函数后返回的 迭代器 是什么。
iterator(迭代器)
我们先来输出一下上文的 迭代器工厂函数 调用后返回的 迭代器:
console.log(arr[Symbol.iterator]()); // Array Iterator {}
我们可以看到他本身是一个对象。这个对象用于迭代于其关联的可迭代对象,在这个例子中就是 arr
。
怎么调用呢?
迭代器使用 next()
方法在 可迭代对象 中遍历数据。每次调用 next()
都会返回一个 iteratorResult对象 ,这个对象中包含两个属性:
-
done
属性:是一个 布尔值,表示本次 迭代 是否到达了 可迭代对象 的 底端。 -
value
属性:包含本次 迭代 在 可迭代对象 中获取的 值(此时done
应为false
)或者undefined
(此时done
应为true
表示已经迭代完成)。
done
的状态为 true
时表示已经迭代完成,此时 value
的值应该为 undefined
,此后再去调用 next()
函数会一直返回同样的值。如下:
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: undefined, done: true}
console.log(iter.next()); // {value: undefined, done: true}
不同 迭代器的实例 之间没有联系,不会互相影响,都能够独立地遍历可迭代对象。如下:
let iter1 = arr[Symbol.iterator]();
let iter2 = arr[Symbol.iterator]();
console.log(iter1.next()); // {value: 1, done: false}
console.log(iter1.next()); // {value: 2, done: false}
console.log(iter2.next()); // {value: 1, done: false}
console.log(iter1.next()); // {value: 3, done: false}
关于迭代器的补充
这里可能会稍微有点绕,结合上文多读几遍就好。
任何一个内置 可迭代接口 返回的 迭代器 本身也是一个 可迭代对象。
既然 迭代器 本身也是一个 可迭代对象,那就意味着这个 迭代器 也实现了 可迭代接口 那么根据前文所说,我们可以在通过 迭代器 中的 可迭代接口,在创建一个 迭代器 ,这里 Symbol.iterator
属性引用的工厂函数会返回相同的迭代器。例子如下:
let iter1 = arr[Symbol.iterator]();
// 这个输出可以看出 iter1 这个 迭代器 也实现了可迭代接口。
console.log(iter1[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
// 调用 iter1 中可迭代接口的工厂函数并赋值给 iter2
let iter2 = iter1[Symbol.iterator]();
// 比较两者
console.log(iter1 === iter2); // true
既然每个 迭代器 也实现了 可迭代接口 ,其本身也是一个 可迭代对象,那么就意味着 它可以用在任何需要 可迭代对象 的地方。例子如下:
let iter = arr[Symbol.iterator]();
// 利用 for..of 的机制内部使用 数组本身 的可迭代接口
for(const i of arr) {
console.log(i);
}
// 1
// 2
// 3
// 利用 for..of 的机制内部使用 迭代器本身 的可迭代接口
for(const i of iter) {
console.log(i);
}
// 1
// 2
// 3
效果是一样的。
这就是为什么我们可以利用 for..of
遍历 Map
实例 或 set
实例 中 entries()
、keys()
和values()
三个方法所返回的 迭代器,因为这些迭代器本身也是可迭代对象,且调用其 可迭代接口 的工厂函数后返回的 迭代器 与本身相等。例子如下:
let mp = new Map();
// 获取 mp 的 迭代器 赋值给iter1
let iter1 = mp.entries();
// 调用 iter1 中的 可迭代接口的工厂函数 返回一个迭代器赋值给iter2
let iter2 = iter1[Symbol.iterator]();
console.log(iter1 === iter2); // true
自定义迭代器
我们知道了 迭代器 的原理后,就可以自定义迭代器,把第一个例子中的 非可迭代对象 obj
变为可迭代对象。如下:
let obj = {
name: 'weilangpu',
age: 19,
hight: 183,
// 可迭代接口 迭代器的工厂函数
[Symbol.iterator]() {
let idx = 0;
let o = this;
let ks = Object.keys(o);
// 返回一个 迭代器对象
return {
// 迭代器对象中包含 next() 方法
next() {
return {
value: o[ks[idx ++]],
done: (idx > ks.length)
};
}
};
}
}
let iter = obj[Symbol.iterator]();
console.log(iter.next()); // {done: false, value: 'weilangpu'}
console.log(iter.next()); // {done: false, value: 19}
console.log(iter.next()); // {done: false, value: 183}
console.log(iter.next()); // {done: true, value: undefined}
console.log(iter.next()); // {done: true, value: undefined}
但是需要注意,这个例子返回的 自定义的迭代器 本身并 不是 一个 可迭代对象,并没有实现可迭代接口,如下:
console.log(iter[Symbol.iterator]); // undefined
当然也可以通过一些办法实现这个效果,这里就不做叙述啦,有兴趣可以翻阅 资料 或 红宝书的第七章 看看。