for...in 与 for...of:一篇彻底讲清“遍历键”与“遍历值”
在 JavaScript 里,for...in 和 for...of 很像,但它们解决的根本不是同一类问题:
for...in:遍历属性名(key)for...of:遍历可迭代对象产出的值(value)
如果你只记语法,很容易写出“能跑但不对”的代码;如果你把它们背后的规则搞清楚,很多面试题和实际 bug 都会变得很好判断。
- 一眼分清
for...in和for...of到底在遍历什么 - 知道它们分别适合数组、对象、字符串、Set、Map 的哪种场景
- 理解“可枚举属性”和“可迭代对象”这两个关键词
- 避开数组遍历、对象遍历里的高频坑
- 文末附带一组高频面试问答
一、先记结论:别混着用
| 对比项 | for...in | for...of |
|---|---|---|
| 遍历结果 | 属性名 / 键名 | 值 |
| 面向对象 | 可枚举属性 | 可迭代对象 |
| 常见返回值 | 字符串键,如 "0"、"name" | 元素值,如 10、"hello" |
| 能否遍历数组 | 能,但不推荐 | 能,推荐 |
| 能否遍历普通对象 | 能 | 不能(默认不可迭代) |
| 能否遍历字符串 | 不推荐 | 推荐 |
能否遍历 Set / Map | 不能正常按值遍历 | 可以 |
| 是否会看原型链 | 会看可枚举的继承属性 | 不关心原型链枚举,按迭代器产出值 |
你可以先用一句话粗暴记忆:
- 想拿 key,先想到
for...in - 想拿 value,先想到
for...of
但这还不够,因为“key”和“value”只是表象,本质差异在下一节。
二、本质差异:一个看“属性”,一个看“迭代器”
2.1 for...in 遍历的是“可枚举属性名”
for...in 会遍历对象上可枚举(enumerable)的字符串属性,并且还会沿着原型链往上找。
先看一个简单例子:
const user = {
name: "Terry",
age: 18,
};
for (const key in user) {
console.log(key);
}
// name
// age
这里输出的是键名,不是值。要拿值,需要再取一次:
for (const key in user) {
console.log(key, user[key]);
}
// name Terry
// age 18
2.2 for...of 遍历的是“迭代器产出的值”
for...of 不关心对象上有哪些属性,它只关心:这个值是不是可迭代(iterable)。
比如数组天然可迭代:
const nums = [10, 20, 30];
for (const value of nums) {
console.log(value);
}
// 10
// 20
// 30
这里拿到的是每一项的值,而不是索引。
三、为什么数组要优先用 for...of,不要用 for...in
这是最常见的面试点,也是最容易写错的地方。
3.1 for...in 遍历数组,拿到的是索引字符串
const arr = [100, 200, 300];
for (const key in arr) {
console.log(key, typeof key);
}
// 0 string
// 1 string
// 2 string
注意:这里的 key 是字符串,不是数字。
如果你只是想拿值,还得多写一步:
for (const key in arr) {
console.log(arr[key]);
}
3.2 数组上自定义属性时,for...in 也会把它遍历出来
const arr = [10, 20];
arr.extra = "额外信息";
for (const key in arr) {
console.log(key);
}
// 0
// 1
// extra
这通常不是你想要的结果。
而 for...of 只会遍历数组迭代器产出的元素值:
for (const value of arr) {
console.log(value);
}
// 10
// 20
3.3 for...in 还可能遍历到原型链上的可枚举属性
这是它更“危险”的地方。只要原型链上有可枚举属性,也可能被扫出来。
const base = { inherited: "来自原型" };
const obj = Object.create(base);
obj.own = "自己的属性";
for (const key in obj) {
console.log(key);
}
// own
// inherited
所以,数组遍历请优先用 for...of;对象遍历若用 for...in,通常要配合 Object.hasOwn() 过滤。
四、普通对象为什么不能直接用 for...of
很多人刚学时会写出这样的代码:
const user = { name: "Terry", age: 18 };
for (const item of user) {
console.log(item);
}
这会直接报错,因为普通对象默认不是可迭代对象。
也就是说,它没有提供默认的迭代器,不知道应该按什么顺序吐出哪些值。
4.1 如果你想遍历对象,推荐这三种写法
写法 1:只要 key
const user = { name: "Terry", age: 18 };
for (const key of Object.keys(user)) {
console.log(key);
}
// name
// age
写法 2:只要 value
for (const value of Object.values(user)) {
console.log(value);
}
// Terry
// 18
写法 3:同时要 key 和 value
for (const [key, value] of Object.entries(user)) {
console.log(key, value);
}
// name Terry
// age 18
实际开发里,如果你想要“像 for...of 一样”遍历对象,最常用的就是:
for (const [key, value] of Object.entries(obj)) {
// ...
}
这也是我最推荐你养成的对象遍历写法。
五、for...in 的正确使用姿势
for...in 不是不能用,而是要知道它到底适合什么。
5.1 适合场景:你明确就是要“对象的属性名”
const config = {
host: "localhost",
port: 3000,
open: true,
};
for (const key in config) {
if (Object.hasOwn(config, key)) {
console.log(key, config[key]);
}
}
这里的 Object.hasOwn(config, key) 很关键。
它表示:
- 只处理对象自身的属性
- 不把原型链上的可枚举属性算进来
5.2 更现代的建议
如果你不是特别需要 for...in 的语义,普通对象遍历更推荐:
for (const [key, value] of Object.entries(config)) {
console.log(key, value);
}
因为它更直观,也不会意外扫到继承属性。
六、for...of 能遍历哪些东西
只要一个值实现了迭代协议,for...of 就能遍历它。
常见可迭代对象有:
- 数组
Array - 字符串
String - 集合
Set - 映射
Map - 某些类数组/DOM 集合(如很多环境下的
NodeList) - 生成器对象
Generator
6.1 遍历字符串
for (const ch of "JS") {
console.log(ch);
}
// J
// S
6.2 遍历 Set
const set = new Set([1, 2, 2, 3]);
for (const value of set) {
console.log(value);
}
// 1
// 2
// 3
6.3 遍历 Map
Map 的默认迭代结果是 [key, value]:
const map = new Map([
["name", "Terry"],
["age", 18],
]);
for (const [key, value] of map) {
console.log(key, value);
}
// name Terry
// age 18
七、如果我既想拿索引,又想拿值,怎么办?
这是 for...of 的另一个高频追问。
7.1 数组用 entries()
const arr = ["a", "b", "c"];
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
// 0 a
// 1 b
// 2 c
这样写比 for...in 更稳,因为:
index是真正的数字索引语义value直接可用- 不会扫到自定义属性和继承属性
八、for...in、for...of、forEach 怎么选
很多同学会把这三者一起问。你可以这样区分:
8.1 for...of
- 适合:数组、字符串、Set、Map 等可迭代对象
- 优点:语义自然,支持
break、continue、return - 缺点:普通对象不能直接用
8.2 for...in
- 适合:遍历对象属性名
- 优点:能直接枚举键名
- 缺点:会受可枚举属性、原型链影响;遍历数组不合适
8.3 forEach
- 适合:数组的函数式遍历
- 优点:写法短
- 缺点:不能直接
break/continue/return跳出外层循环
看一个对比例子:
const nums = [1, 2, 3, 4, 5];
for (const n of nums) {
if (n === 3) break;
console.log(n);
}
// 1
// 2
如果你想中途停止,for...of 往往比 forEach 更合适。
九、几个很容易混淆的细节
9.1 for...in 遍历到的是字符串键
const arr = [10, 20];
for (const key in arr) {
console.log(key, typeof key);
}
// 0 string
// 1 string
所以不要期待它天然就是数字索引。
9.2 for...in 不遍历 Symbol 键
const id = Symbol("id");
const obj = {
name: "Terry",
[id]: 1001,
};
for (const key in obj) {
console.log(key);
}
// name
如果你要拿到对象全部自有键(包括 Symbol),更适合用:
Reflect.ownKeys(obj);
9.3 for...of 的前提是对象可迭代
如果一个对象实现了 Symbol.iterator,它也可以被 for...of 遍历。只是普通对象默认没有这项能力。
你可以先把这件事理解成:
for...in:看对象“有哪些可枚举属性名”for...of:看对象“愿意按什么规则一个个吐出值”
十、开发中的推荐写法清单
10.1 遍历数组
for (const item of list) {
// 推荐
}
10.2 遍历数组并拿索引
for (const [index, item] of list.entries()) {
// 推荐
}
10.3 遍历普通对象
for (const [key, value] of Object.entries(obj)) {
// 推荐
}
10.4 明确要枚举对象键名
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
// 可以用
}
}
10.5 尽量避免的写法
for (const key in arr) {
// 不推荐遍历数组
}
十一、一组例子帮你彻底记住
const obj = { a: 1, b: 2 };
const arr = [10, 20];
const str = "hi";
for (const key in obj) {
console.log("obj in:", key);
}
for (const value of arr) {
console.log("arr of:", value);
}
for (const ch of str) {
console.log("str of:", ch);
}
输出含义分别是:
obj in:对象的键名arr of:数组的元素值str of:字符串的每个字符
如果你能把这三句解释顺,就说明你已经真的分清了。
十二、高频面试问答
Q1:for...in 和 for...of 的核心区别是什么?
A: for...in 遍历的是对象的可枚举属性名;for...of 遍历的是可迭代对象产生的值。一个面向“属性枚举”,一个面向“迭代协议”。
Q2:为什么不推荐用 for...in 遍历数组?
A: 因为它拿到的是字符串索引,而且还可能遍历出自定义属性、继承的可枚举属性,语义上不稳定。数组更适合用 for...of、forEach、普通 for。
Q3:为什么普通对象不能直接 for...of?
A: 因为普通对象默认不是可迭代对象,没有内置的 Symbol.iterator。如果要遍历对象,通常先用 Object.keys()、Object.values()、Object.entries() 转成可迭代结果再配合 for...of。
Q4:for...in 会遍历原型链上的属性吗?
A: 会。只要原型链上的属性是可枚举的,就可能被遍历到。所以实际开发里常配合 Object.hasOwn(obj, key) 过滤。
Q5:for...of 能不能遍历对象的 key?
A: 能,但不是直接遍历普通对象本身,而是遍历 Object.keys(obj) 的结果:
for (const key of Object.keys(obj)) {
console.log(key);
}
Q6:for...of 和 forEach 相比有什么优势?
A: for...of 支持 break、continue、return,更适合需要中途终止或跳过的场景;forEach 更偏函数式回调风格。
Q7:如何在 for...of 里同时拿到索引和值?
A: 用 entries():
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
十三、最后用 4 句话收尾
for...in遍历的是 key。for...of遍历的是 value。- 数组优先
for...of,对象优先Object.entries() + for...of。 - 如果你非要用
for...in遍历对象,记得加Object.hasOwn()。
把这四句记住,这个知识点基本就稳了。