for…in 与 for…of 的区别:谁遍历的是 Key?谁遍历的是 Value?谁能遍历 Set/Map?

for…in 与 for…of 的区别:深入理解 JavaScript 中的两种循环机制

大家好,欢迎来到今天的编程技术讲座!我是你们的讲师,今天我们要聊一个看似简单却非常关键的话题——for...infor...of 的区别。这两个语法结构在日常开发中频繁出现,但很多人对其行为理解模糊,甚至误用导致 bug。尤其当你开始使用 ES6+ 新特性(如 SetMap)时,这种混淆会更加明显。

本文将从基础原理讲起,逐步深入到实际应用场景,并通过大量代码示例帮你彻底搞清楚:

  • 哪个遍历的是 Key
  • 哪个遍历的是 Value
  • 它们各自能遍历哪些数据结构?

我们还会对比它们在性能、语义清晰度和兼容性方面的差异,最后给出最佳实践建议。


一、核心概念回顾:什么是 for…in?什么是 for…of?

✅ for…in:基于对象属性名的迭代

for...in 是最早出现在 JavaScript 中的循环方式之一,主要用于遍历对象的所有可枚举属性名(即 key)。它的本质是“按键访问”。

const obj = { a: 1, b: 2, c: 3 };

for (let key in obj) {
    console.log(key);     // 输出: a, b, c
    console.log(obj[key]); // 输出: 1, 2, 3
}

🔍 注意:for...in 遍历的是 key(属性名),不是值!

✅ for…of:基于 iterable 协议的迭代

for...of 是 ES6 引入的新语法,它依赖于对象是否实现了 iterable 协议(即具有 [Symbol.iterator] 方法)。它可以用来遍历数组、字符串、Set、Map 等所有实现了该协议的数据结构。

const arr = [10, 20, 30];

for (let value of arr) {
    console.log(value); // 输出: 10, 20, 30
}

🔍 注意:for...of 遍历的是 value(元素本身),而不是索引或键!


二、谁遍历的是 Key?谁遍历的是 Value?

循环类型 遍历内容 示例 输出结果
for...in Key(属性名) for (let k in obj) "a", "b", "c"
for...of Value(实际值) for (let v of arr) 10, 20, 30

📌 结论明确:

  • for...in → 遍历的是 Key
  • for...of → 遍历的是 Value

但这只是表面现象。真正决定这个行为的是底层机制的不同:

⚙️ for…in 的工作原理

它本质上是调用了 Object.keys() 或者类似逻辑来获取对象的可枚举属性列表,然后逐个访问这些 key。

const obj = { name: 'Alice', age: 25 };

// 等价于下面这段代码
for (let key in obj) {
    console.log(key); // name, age
}

// 实际上相当于:
Object.keys(obj).forEach(key => {
    console.log(key);
});

⚠️ 特别提醒:如果你对一个普通对象使用 for...of,会报错!因为普通对象没有实现 Symbol.iterator 接口。

const obj = { a: 1 };
for (let x of obj) { 
    // TypeError: obj is not iterable
}

三、谁能遍历 Set / Map?

这是最容易出错的地方!让我们分别测试这两种结构。

🧪 测试:Set 结构

const set = new Set(['apple', 'banana', 'cherry']);

// for...in ❌ 不适用
for (let key in set) {
    console.log(key); // 无输出(不会执行)
}

// for...of ✅ 正确!
for (let value of set) {
    console.log(value); // apple, banana, cherry
}

结论:只有 for...of 能遍历 Set,因为它实现了 Symbol.iterator。

🧪 测试:Map 结构

const map = new Map([
    ['name', 'Alice'],
    ['age', 25],
    ['city', 'Beijing']
]);

// for...in ❌ 不适用
for (let key in map) {
    console.log(key); // 无输出
}

// for...of ✅ 正确!
for (let entry of map) {
    console.log(entry); // ['name', 'Alice'], ['age', 25], ['city', 'Beijing']
}

🔍 进一步拆解 Map 的 entries:

for (let [key, value] of map) {
    console.log(`${key}: ${value}`); // name: Alice, age: 25, city: Beijing
}

💡 总结:
| 数据结构 | for…in 支持? | for…of 支持? | 说明 |
|———-|——————|——————|——-|
| Object(普通对象) | ✅ 是(遍历 key) | ❌ 否(未实现 Iterator) | 使用 for...in 遍历 key,for...of 报错 |
| Array(数组) | ✅ 是(遍历 index) | ✅ 是(遍历 value) | 注意:for...in 通常用于索引,for...of 更推荐用于元素 |
| String(字符串) | ✅ 是(遍历 index) | ✅ 是(遍历 char) | for...in 可以用,但不如 for...of 清晰 |
| Set(集合) | ❌ 否 | ✅ 是 | 必须用 for...of |
| Map(映射) | ❌ 否 | ✅ 是 | 必须用 for...of,且可解构为 key-value 对 |


四、常见陷阱与误区分析

❗ 误区一:认为 for…in 可以安全地遍历数组

const arr = ['a', 'b', 'c'];

for (let i in arr) {
    console.log(i); // 输出: "0", "1", "2"
}

看起来没问题?其实不然!这里你得到的是 字符串形式的索引,而非真正的数字索引。如果做数学运算容易出错:

for (let idx in arr) {
    console.log(typeof idx); // string!
    console.log(idx + 1);    // "01", "11", "21" —— 错误!
}

✅ 正确做法:用 for...of 来遍历数组元素:

for (let val of arr) {
    console.log(val); // a, b, c
}

或者用传统的 for 循环处理索引:

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

❗ 误区二:认为 for…of 可以遍历任意对象

const obj = { x: 1, y: 2 };
for (let item of obj) {
    console.log(item); // TypeError: obj is not iterable
}

原因很简单:普通对象没有实现 Symbol.iterator,所以不能被 for...of 遍历。

👉 如果你想让普通对象支持 for...of,必须手动添加迭代器方法:

const obj = { a: 1, b: 2 };
obj[Symbol.iterator] = function* () {
    for (let key in this) {
        yield this[key];
    }
};

for (let val of obj) {
    console.log(val); // 1, 2
}

⚠️ 但这种方式不推荐,除非你真的需要自定义行为。否则应该选择合适的容器结构(如 Map/Array)。


五、性能对比与场景选择建议

场景 推荐语法 原因
遍历对象属性(key/value) for...in 最直接,适合读取对象字段
遍历数组元素 for...of 更简洁,避免索引问题
遍历 Set / Map for...of 唯一合法方式
需要索引位置 for 循环(传统) for...in 返回字符串索引,易出错
自定义迭代逻辑 手动实现 Symbol.iterator 适用于特殊需求

💡 性能小实验(简化版)

虽然两者性能差距不大,但在某些极端情况下可以观察到差异:

const largeArr = Array.from({ length: 100000 }, (_, i) => i);

console.time('for...');
for (let i = 0; i < largeArr.length; i++) {
    // do nothing
}
console.timeEnd('for...'); // ~1ms

console.time('for...of');
for (let val of largeArr) {
    // do nothing
}
console.timeEnd('for...of'); // ~2ms

📌 在大多数场景下,for...of 更易读、更安全;而 for 循环更适合需要索引的操作。


六、现代 JS 开发中的最佳实践

✅ 推荐写法清单

✔️ 遍历对象属性(key & value)

const user = { name: 'Bob', age: 30 };

// 获取 key 和 value
for (let key in user) {
    console.log(`${key}: ${user[key]}`);
}

// 或者使用 Object.entries()
for (let [key, value] of Object.entries(user)) {
    console.log(`${key}: ${value}`);
}

✔️ 遍历数组元素(推荐 for…of)

const fruits = ['apple', 'banana', 'orange'];

for (let fruit of fruits) {
    console.log(fruit);
}

✔️ 遍历 Set / Map(唯一方式)

const mySet = new Set(['a', 'b']);
for (let item of mySet) {
    console.log(item);
}

const myMap = new Map([['k1', 'v1'], ['k2', 'v2']]);
for (let [key, value] of myMap) {
    console.log(`${key} -> ${value}`);
}

❌ 避免错误用法

// ❌ 错误:试图用 for...of 遍历普通对象
const obj = { a: 1 };
for (let x of obj) {} // TypeError!

// ❌ 错误:用 for...in 遍历数组元素(可能误导)
for (let idx in arr) {
    console.log(arr[idx]); // 表面上可行,但 idx 是字符串,容易引发 bug
}

七、结语:掌握底层逻辑,写出健壮代码

今天我们系统梳理了 for...infor...of 的本质区别:

  • for...in → 遍历的是 key(属性名)
  • for...of → 遍历的是 value(元素值)
  • Set / Map 只能用 for…of 遍历
  • 普通对象不能用 for…of,除非手动实现 Iterator

记住一句话:

“当你要遍历‘东西’的时候,用 for…of;当你想遍历‘名字’的时候,用 for…in。”

这不仅是语法层面的选择,更是思维方式的转变:从“我怎么拿到数据”变成“我如何正确表达意图”。

希望今天的讲解让你不再混淆这两个关键字,而是能在项目中自信地做出正确的选择。下次遇到遍历问题时,请先问自己:“我要遍历的是 key 还是 value?” —— 答案往往就藏在这两个词之间。

谢谢大家!欢迎留言讨论你的实战经验 👇

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注