for...in 与 for...of:区别、底层模型与适用场景
面试速答(30 秒版 TL;DR)
for...in遍历的是可枚举属性名(key),本质是“属性枚举”;会拿到字符串键,还可能扫到原型链上的可枚举属性。for...of遍历的是可迭代对象产出的值(value),本质是“迭代器协议(iterator protocol)”。- 数组、字符串、
Map、Set、NodeList这类场景优先for...of;普通对象通常用Object.keys/entries配合for...of。 - 一句话记忆:
for...in看 key,for...of看 value;前者偏对象枚举,后者偏集合遍历。
心智模型:for...in 看“属性枚举”,for...of 看“迭代器协议”
for...in 遍历的到底是什么
for...in 遍历的是:对象上(以及原型链上)可枚举(enumerable)的字符串键。
const obj = Object.create({ inherited: 1 });
obj.own = 2;
for (const k in obj) {
console.log(k);
}
// 可能输出:own inherited(包含原型链上的 enumerable 属性)
如果你只想要“自有属性”,面试里一定要补这一句:
for (const k in obj) {
if (!Object.hasOwn(obj, k)) continue; // 或 obj.hasOwnProperty(k)
// k 一定是自有属性名
}
for...of 遍历的到底是什么
for...of 遍历的是:实现了 iterable 的对象的“迭代结果”,也就是调用 obj[Symbol.iterator]() 返回 iterator,不断 next() 取 { value, done }。
const arr = [10, 20];
for (const v of arr) {
console.log(v); // 10 20
}
普通对象默认不是 iterable,所以不能直接 for...of obj:
const obj = { a: 1 };
for (const x of obj) {
// TypeError: obj is not iterable
}
要遍历对象键值对,推荐:
const obj = { a: 1, b: 2 };
for (const [k, v] of Object.entries(obj)) {
console.log(k, v);
}
一张表说清差异
| 维度 | for...in | for...of |
|---|---|---|
| 遍历目标 | 属性名(key) | 迭代产出的值(value) |
| 底层机制 | 属性枚举 | Symbol.iterator |
| 适用对象 | 普通对象 | iterable:数组、字符串、Map、Set 等 |
| 是否会拿到原型链属性 | 可能会 | 不会按原型链枚举 |
| 遍历数组是否推荐 | 不推荐 | 推荐 |
| 取到的内容 | "0"、"1" 这类键 | 10、20 这类值 |
典型差异点(高频追问)
1) 遍历数组:为什么不推荐 for...in
for...in 对数组来说会遍历“属性名”,不只包含索引,还可能包含你挂上去的自定义属性,甚至原型链上的 enumerable 属性。
const a = [10, 20];
a.foo = 1;
for (const k in a) console.log(k); // "0" "1" "foo"(顺序也不该依赖)
for (const v of a) console.log(v); // 10 20(只走迭代值)
结论(面试口径):
- 数组遍历:优先
for...of/ 传统for/forEach(不需要 break)等。 for...in更像是“对象属性枚举”,不是“数组元素遍历”。
2) 稀疏数组(holes):for...of 会走,for...in 会跳过
const a = new Array(3); // [ <3 empty items> ]
for (const v of a) console.log(v); // undefined undefined undefined
for (const k in a) console.log(k); // 什么都不输出
面试要点:
for...of是按迭代器按索引推进,取值时“拿不到就是undefined”。for...in枚举的是实际存在的可枚举属性键;洞位没有对应属性键,所以会跳过。
3) Map/Set:for...of 更自然
const m = new Map([
["a", 1],
["b", 2],
]);
for (const [k, v] of m) {
console.log(k, v);
}
const s = new Set([1, 2, 3]);
for (const v of s) console.log(v);
4) 需要 index:for...of 用 entries()
const arr = ["x", "y"];
for (const [i, v] of arr.entries()) {
console.log(i, v); // 0 "x" / 1 "y"
}
5) Symbol 键怎么办
for...in 不会枚举 Symbol 键;如果对象上存在 Symbol 属性,需要:
const sym = Symbol("id");
const obj = { a: 1, [sym]: 2 };
console.log(Object.keys(obj)); // ["a"]
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(id)]
面试要点:
for...in面向的是“字符串可枚举键”。- 真正想完整拿到对象自有键,需要结合
Reflect.ownKeys()。
6) 异步迭代怎么办
如果对象实现的是 Symbol.asyncIterator,要用 for await...of:
async function* gen() {
yield 1;
yield 2;
}
(async () => {
for await (const x of gen()) {
console.log(x);
}
})();
典型题 & 标准答法
Q1:遍历对象用哪个更好?
推荐答法:
- 只遍历自有 key/value:
Object.keys/values/entries+for...of。 - 需要包含原型链属性(少见):才考虑
for...in,并明确这是“枚举属性”的语义。
Q2:for...of 还能遍历什么?能不能遍历异步数据?
要点:
- 只要实现了
Symbol.iterator就能for...of(数组、字符串、Map/Set、生成器、DOM 集合等)。 - 异步可迭代用
for await...of(Symbol.asyncIterator),常见于流式数据/异步生成器。
Q3:为什么普通对象不能直接 for...of
因为普通对象默认没有实现 Symbol.iterator,它只是一组属性集合,不是语言层面的 iterable。
标准答法:
- “普通对象适合做属性容器,不适合直接做迭代源。”
- “要遍历它,先把它转换成数组形态,例如
Object.entries(obj)。”
易错点/坑
- 把
for...in当“数组遍历”:容易踩到自定义属性、原型链属性、洞位等问题。 for...in没有自有属性过滤:不加Object.hasOwn可能枚举到继承属性。for...of不能直接遍历普通对象:要用Object.entries或自己实现迭代器。
速记要点(可背诵)
for...in:枚举 key(可枚举属性名,含原型链);对象场景多,数组慎用。for...of:迭代 value(走Symbol.iterator);数组/字符串/Map/Set首选。- 遍历普通对象:优先
Object.entries(obj)+for...of。