for...in
for...in 用来遍历对象的可枚举属性。很多人第一次接触它时,会把它理解成'专门遍历数组下标'的语法,这种说法不算错,但只说了一半。它更准确的职责,是按属性名去遍历一个对象,而且会把原型链上可枚举的属性也一起带出来。
先看一个最常见的场景:
const arr = [1, 2, 3, 4, 5];
const obj = {
mark: 'mark-v',
jack: 'jack-v',
amy: 'amy-v',
};
for (const i in arr) {
// 输出 0 1 2 3 4
console.log(i);
}
for (const i in obj) {
// 输出 mark jack amy
console.log(i);
}
数组里输出的是下标,对象里输出的是键名。这个结果并不意外,但它也容易让人误以为 for...in 的能力就这么点。实际上,真正值得注意的,是它背后处理属性的规则。
继续往下看:
const arr = [1, 2, 3, 4, 5];
arr.value = 'array'; // 给数组对象额外加一个属性
const s1 = Symbol('symbol1');
const s2 = Symbol('symbol2');
const obj = {
mark: 'mark-v',
jack: 'jack-v',
amy: 'amy-v',
[s1]: 'symbol1-v',
[s2]: 'symbol2-v',
};
// 给原型链上挂一个可枚举属性
Object.getPrototypeOf(obj).myProto = 'proto';
for (const i in arr) {
console.log(i);
// 输出 0 1 2 3 4 value myProto
}
for (const i in obj) {
console.log(i);
// 输出 mark jack amy myProto
}
这里有三个点很关键:
for...in只遍历可枚举属性。Symbol类型的键会被跳过,哪怕它本身也是可枚举的。for...in会继续沿着原型链找可枚举属性。
所以 arr 能遍历到 value 和 myProto,obj 也能遍历到 myProto。原因不是数组和对象'共享了某种特殊能力',而是它们的原型链最终都会碰到 Object.prototype。
Object.getPrototypeOf(obj) === Object.getPrototypeOf(Object.getPrototypeOf(arr)); // true
这也说明了一个容易被忽略的事实:for...in 不是只看'这个对象自己身上有什么',它还会顺着原型链继续往上扫。实际开发里,如果你只想遍历对象自身属性,通常要配合 hasOwnProperty 做过滤。
什么是可枚举属性
可枚举属性指的是属性描述符里 enumerable 为 true 的属性。
- 通过对象字面量定义的属性,默认就是
true - 通过
Object.defineProperty定义的属性,默认是false Symbol键也可以是可枚举的,只是for...in不会遍历它们
看一个更完整的例子:
const s1 = Symbol('symbol1');
const s2 = Symbol('symbol2');
const obj = {
mark: 'mark-v',
};
obj.jack = 'jack-v';
obj['amy'] = 'amy-v';
Object.defineProperty(obj, 'kong', {
value: 'kong-v',
enumerable: false,
});
Object.defineProperty(obj, 'john', {
value: 'john-v',
enumerable: true,
});
Object.defineProperty(obj, s1, {
value: 's1-v',
enumerable: false,
});
Object.defineProperty(obj, s2, {
value: 's2-v',
enumerable: true,
});
for (const i in obj) {
console.log(i);
// 输出 mark jack amy john
}
这里能看到,kong 没有被遍历出来,因为它是不可枚举属性;john 可以被遍历到,因为我们显式把它设成了可枚举。至于两个 Symbol 键,即使 s2 设置成了 enumerable: true,for...in 还是会跳过它。
数组下标也是属性
数组看起来是'有顺序的一组值',但从对象模型上看,它的下标本质上也是属性。你可以把数组当成一种特殊对象来理解。
const arr = [1, 2, 3, 4, 5];
Object.defineProperty(arr, 0, {
value: 100,
enumerable: false,
writable: false,
});
arr[0] = 1;
console.log(arr); // [ 100, 2, 3, 4, 5 ]
for (const i in arr) {
console.log(i);
// 输出 1 2 3 4
}
这段代码很适合帮助理解'数组下标也是属性'这件事:
- 把
0号下标设为不可枚举后,for...in遍历不到它 - 把
writable设为false后,直接赋值也改不动它 - 但数组本身打印出来时,依然会显示完整内容,因为控制台展示的是对象的当前状态,不等于
for...in的遍历规则
for...of
for...of 的思路就完全不一样了。它不是按属性名来遍历对象,而是遍历可迭代对象的值。只要一个对象实现了迭代器接口,就可以被 for...of 消费。
常见的可迭代对象包括:数组、字符串、Map、Set 等。
// 迭代数组
const array = [10, 20, 30];
for (const value of array) {
console.log(value); // 10 20 30
}
// 迭代字符串
const string = 'kong';
for (const s of string) {
console.log(s); // k o n g
}
// 迭代 Map
const map = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
for (const item of map) {
console.log(item); // [ 'a', 1 ] [ 'b', 2 ] [ 'c', 3 ]
}
// 迭代 Set
const set = new Set([1, 1, 2, 2, 3, 3]);
for (const item of set) {
console.log(item); // 1 2 3
}
如果你注意观察,会发现 for...of 关心的是'值',而不是'键'。这也是它和 for...in 最本质的区别。
迭代器可以被中断
for...of 循环可以被 break、throw 或 return 终止。迭代器在这些情况下会被关闭,这在处理流式数据或自定义迭代器时尤其重要。
const array = [10, 20, 30];
for (const value of array) {
console.log(value);
return;
}
// 下面的代码不会执行
const string = 'kong';
for (const s of string) {
console.log(s);
}
const array = [10, 20, 30];
for (const value of array) {
console.log(value);
throw new Error();
}
// 不会执行到这里
const string = 'kong';
for (const s of string) {
console.log(s);
}
这一点和 for...in 类似:循环体里的 break、throw、return 都能让循环提前结束。不同的是,for...of 背后牵涉的是迭代器协议,终止时还会做相应的关闭处理。
总结
for...in用来遍历对象的可枚举属性名,会遍历对象自身以及原型链上的可枚举属性,但会跳过Symbol键for...of用来遍历可迭代对象的值,常见对象有数组、字符串、Map、Set- 如果你想遍历数组元素,优先考虑
for...of - 如果你确实需要拿到对象的键名,并且还要处理原型链上的枚举属性,
for...in才是对应的工具
理解这两者,关键不在于记住表面现象,而在于分清:一个是在遍历属性名,一个是在遍历值。只要把这个边界拎清楚,面试里常见的追问也就不难接了。
