小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
通过本篇文章,你可了解到以下知识点:
for of的常用用法- 和其他循环的区别
- 不能遍历
Object对象?- 如何遍历
Object对象?- 关乎
for of不能遍历的Iterator介绍- 如何给
Object增加Symbol.iterator方法
如果你想了解更详细的解说,请认真往下阅读;如果你想快速查看结论,请看『总结』目录。
前言
随着前端的不断发展,光循环就出现了好多种方法,for、forEach、do..while、for...in等等,不过这些循环也都各有各的应用场景和优缺点。
ES6又为我们提供了新的循环方法for...of,它可以循环字符串、数组及其他类数组对象,那作为最普遍存在的Object对象,按理,可以循环?
我们看一下下方的代码示例:
{
// 迭代数组
const iterable = ['a', 'b'];
for (const value of iterable) {
console.log(value);
}
// output: a b
}
{
// 普通对象
const obj = {
a: 'A',
b: 'B'
}
for(const item of obj){
console.log(item)
}
// Uncaught TypeError: obj is not iterable
}
oh no,报错了:Uncaught TypeError: obj is not iterable。提示obj是不可迭代的,显然直接用for...of去遍历Object对象是不行的。
本篇文章就带你深入了解一下,可以遍历大部分数据结构的for...of为何不能遍历Object对象。
for of的概念和常用用法
概念
ES6引入for...of循环,作为遍历所有数据结构的统一的方法,创建一个循环来迭代可迭代的数据结构,包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
语法
- variable:每个迭代的属性值被分配给该变量。
- iterable:一个具有可枚举属性并且可以迭代的对象。
for (variable of iterable) {
statement
}
常用用法
数组
数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器。
{
// 迭代数组
const iterable = ['a', 'b'];
for (const value of iterable) {
console.log(value);
}
// output: a b
}
对比for...in循环,for...in循环只能获得对象的键名,不能直接获取键值。而ES6提供的for...of允许遍历获得键值。
const iterable = ['a', 'b', 'c', 'd'];
for (let value in iterable) { // value为键名索引
console.log(value);
}
// output: 0 1 2 3
for (let value of iterable) { // value为键值,如需获取索引可以借助数组实例的`entries`方法和`keys`方法
console.log(value);
}
// output: a b c d
Set和Map结构
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。
const iterable = new Set(["a", "b", "b", "c"]);
for (let value of iterable) {
console.log(value);
}
// output: a b c
const iterable = new Map();
es6.set("a", 1);
es6.set("b", 2);
es6.set("c", 3);
for (let [name, value] of iterable) {
console.log(name + ": " + value);
}
// output:
// a: 1
// b: 2
// c: 3
类数组对象
1. 字符串
// 字符串
let str = "hello";
for (let s of str) {
console.log(s); // h e l l o
}
2. DOM NodeList 对象
// DOM NodeList对象
let paras = document.querySelectorAll("p");
for (let p of paras) {
p.classList.add("test");
}
3. arguments对象
// arguments对象
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// 'a'
// 'b'
对象(不能使用for...of遍历)
// 普通对象
const obj = {
a: 'A',
b: 'B'
}
for(const item of obj){
console.log(item)
}
// Uncaught TypeError: obj is not iterable
我们发现,对于普通的对象,for...of结构不能直接使用,会报错,提示obj is not iterable,也就是说普通对象默认没有Iterator接口,必须部署了 Iterator 接口后才能使用。
和其他循环的区别
| 循环名称 | 循环对象 | 是否可中断循环 | 是否有返回值 |
|---|---|---|---|
for | for循环体的length | 可以 | 无返回值 |
forEach | 仅可循环数组、map、set等,不可循环字符串、普通对象 | 不可以 | 无返回值 |
do...while | 满足某种条件,则可一直循环,至少循环一次 | 可以 | 无返回值 |
while | 满足某种条件,则可一直循环 | 可以 | 无返回值 |
map | 组成新的数组成员,仅可循环数组,不可循环字符串、普通对象,set、map | 不可中断 | 返回新数组,不影响原数组 |
filter | 过滤数组成员,仅可循环数组,不可循环字符串、普通对象,set、map | 不可中断 | 返回新数组,不影响原数组 |
for...in | 可循环数组、对象,不可循环map、set。可遍历数字键名,还可遍历手动添加的其他键,甚至包括原型链上的键 | 可以 | 无返回值 |
for...of | 循环可迭代的对象,不可循环普通对象(统一数据结构遍历) | 可以 | 无返回值 |
Array.prototype.newArr = () => {};
Array.prototype.anotherNewArr = () => {};
const array = ['foo', 'bar', 'baz'];
for (const value in array) {
console.log(value); // 0 1 2 newArr anotherNewArr
}
for (const value of array) {
console.log(value); // 'foo', 'bar', 'baz'
}
不能遍历Object对象?
在for...of介绍章节,我们提到了一个可迭代数据结构。那什么是可迭代数据结构呢?
实际上,任何具有 Symbol.iterator 属性的数据结构都是可迭代的。
当用for...of迭代普通对象时,会报obj is not iterable。也就是说普通对象是不可迭代的,没有Symbol.iterator属性。
我们查看几个可被for...of迭代的对象原型,看看和普通对象有何不同:
Set数据结构
Array数组
Object对象
可以看到,这些可被for of迭代的对象,都实现了一个Symbol(Symbol.iterator)方法,而普通对象没有这个方法。
简单来说,for...of 语句创建一个循环来迭代可迭代的对象,可迭代的对象内部实现了Symbol.iterator方法,而普通对象没有实现这一方法,所以普通对象是不可迭代的。
关乎for of不能遍历的Iterator介绍
ES6 为了统一集合类型数据结构的处理,增加了 iterator 接口,供 for...of 使用,简化了不同结构数据的处理。而 iterator 的遍历过程,则是类似 Generator 的方式,迭代时不断调用next方法,返回一个包含value(值)和done属性(标识是否遍历结束)的对象。
如何遍历Object对象
通过前面的用法示例,我们知道,for...of可以迭代数组、Map等数据结构,顺着这个思路,我们可以结合对象的Object.values()、Object.keys()、Object.entries()方法以及解构赋值的知识来用for...of遍历普通对象。
1. Object扩展方法values()、keys()、entries()
Object.values()返回普通对象的键值组成的数组
Object.keys()返回普通对象的键名组成的数组
Object.entries()返回普通对象的键名键值组成的二维数组
const obj = {
a: 'A',
b: 'B'
}
// Object.values()
console.log(Object.values(obj)) // ["A", "B"]
// Object.keys()
console.log(Object.keys(obj)) // ["a", "b"]\
// Object.entries()
console.log(Object.entries(obj)) // [["a","A"],["b","B"]]
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
// a A
// b B
2. Generator 函数重新包装对象
const obj = { a: 1, b: 2, c: 3 }
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
Object增加Symbol.iterator方法
给普通对象增加Symbol.iterator方法,不用担心[Symbol.iterator]属性会被Object.keys()获取到导致遍历结果出错,因为Symbol.iterator这样的Symbol属性,需要通过Object.getOwnPropertySymbols(obj)才能获取,Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。
// 普通对象
const obj = {
a: 'A',
b: 'B',
[Symbol.iterator]() {
// 这里Object.keys不会获取到Symbol.iterator属性
const keys = Object.keys(obj);
let index = 0;
return {
next: () => {
if (index < keys.length) {
// 迭代结果 未结束
return {
value: this[keys[index++]],
done: false
};
} else {
// 迭代结果 结束
return { value: undefined, done: true };
}
}
};
}
}
// 可以直接使用for of遍历obj对象了
for (const value of obj) {
console.log(value); // A B
};
// 也可以将对象转成数组
console.log([...obj]); // ['A', 'B']
默认调用Iterator接口的场合
- 扩展运算符(...):ES6数组增加了扩展运算符,任何定义了遍历器(
Iterator)接口的对象,都可以用扩展运算符转为真正的数组。也就是说,只要某个数据结构部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组(毫不意外的,代码[...{}]会报错,而[...'123']会输出数组['1','2','3'])。 - 变量的结构赋值: 数组和可迭代对象的解构赋值(解构是ES6提供的语法糖,其实内在是针对
可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值。而普通对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。 - yield*:
_yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口; - 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用;
- 字符串:S6 为字符串添加了遍历器接口,使得字符串可以被
for...of循环遍历。
总结
for of不能遍历Object对象?
for...of不能遍历Object是因为for...of内部没有iterator接口,没有Symbol.iterator()这个属性,for...of只能遍历有iterator接口的数据结构。
for of如何遍历Object对象?
1. Object扩展方法
Object.values()返回普通对象的键值组成的数组
Object.keys()返回普通对象的键名组成的数组
Object.entries()返回普通对象的键名键值组成的二维数组
使用Object扩展方法处理后的数组,再进行for...of遍历。
2. Generator 函数重新包装对象
明细看具体章节
3. 给Object增加Symbol.iterator属性
明细看具体章节
本篇文章到这里就结束啦!知识有木有多一点点~~~
一起努力!一起进步!