🔄 遍历的艺术:深入解析 for, for...in, for...of 的核心区别
🤔 为什么会有这么多循环?
JavaScript 发展至今,保留了多种循环方式,主要是因为它们解决的问题不同:
for(传统循环):最基础、最灵活,控制力最强,但语法繁琐。for...in:专为对象属性遍历设计,但容易踩坑(遍历原型链)。for...of:ES6 新增,专为**可迭代对象(Iterable)**设计,简洁且安全。
通俗比喻:
for循环 :像手动驾驶。你需要自己控制油门(初始化)、方向盘(条件判断)和刹车(步进)。虽然累,但想去哪就去哪,甚至可以在半路停下来做别的事。for...in:像查户口。它不管你是不是"居民"(自有属性),连你隔壁邻居借住的人(原型链属性)都给你列出来。适合检查对象里有哪些"名字",但不适合处理有序列表。for...of:像坐地铁。你只需要上车(获取迭代器),它会自动把你带到每一站(元素)。你只关心站点(值),不关心轨道是怎么铺的(索引/键)。而且,地铁支持"中途下车"(break/return),体验极佳。
📂 目录
- [🔍 三大循环全景对比](#🔍 三大循环全景对比)
- [🚀 传统 for 循环:基石与灵活性](#🚀 传统 for 循环:基石与灵活性)
- [⚠️ for...in:对象属性的陷阱](#⚠️ for…in:对象属性的陷阱)
- [✨ for...of:现代开发的优选](#✨ for…of:现代开发的优选)
- [🧬 底层原理:迭代器协议 (Iterator Protocol)](#🧬 底层原理:迭代器协议 (Iterator Protocol))
- [💡 最佳实践总结](#💡 最佳实践总结)
1. 🔍 三大循环全景对比
先看一张表,快速了解核心差异:
| 特性 | for (传统) |
for...in |
for...of |
|---|---|---|---|
| 适用对象 | 任何可索引结构 (数组、类数组) | 对象 (Object)、数组(不推荐) | 可迭代对象 (Array, String, Map, Set, NodeList等) |
| 返回值 | 索引 (Index) / 自定义变量 | 键名 (Key) / 属性名 | 键值 (Value) / 元素值 |
| 遍历原型链 | ❌ 否 | ✅ 是 (需配合 hasOwnProperty) |
❌ 否 |
| 遍历顺序 | 索引顺序 | 无序 (规范未强制保证顺序) | 插入顺序 / 定义顺序 |
| 异步支持 | ✅ 支持 async/await |
❌ 不支持 (通常同步) | ✅ 支持 async/await |
| 性能 | ⭐⭐⭐⭐⭐ (最快) | ⭐⭐ (较慢,涉及原型查找) | ⭐⭐⭐⭐ (略低于 for,但差异极小) |
| 可读性 | 低 (样板代码多) | 中 | 高 (语义清晰) |
2. 🚀 传统 for 循环:基石与灵活性
这是最古老的循环方式,也是性能最高的。
✅ 基本用法
javascript
const arr = ["a", "b", "c"];
for (let i = 0; i < arr.length; i++) {
console.log(`Index: ${i}, Value: ${arr[i]}`);
}
// Output:
// Index: 0, Value: a
// Index: 1, Value: b
// Index: 2, Value: c
💡 优势与场景
- 完全控制 :你可以随意修改
i的值(如i += 2跳着遍历),或者在中途改变终止条件。 - 高性能:没有额外的函数调用开销或迭代器创建开销,适合海量数据遍历。
- 异步友好 :在
async函数中,它可以完美配合await实现串行执行。
javascript
// 场景:需要按顺序等待每个异步任务完成
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
await doSomethingAsync(items[i]); // 串行:做完第一个再做第二个
}
}
⚠️ 缺点
- 样板代码多 :需要定义计数器、判断条件、步进表达式,容易写错(如
<写成<=导致越界)。 - 变量泄露风险 :如果使用
var而不是let,计数器会泄露到外部作用域。
3. ⚠️ for...in:对象属性的陷阱
for...in 是为遍历对象的可枚举属性 设计的,千万不要用来遍历数组!
✅ 基本用法
javascript
const obj = { name: "Alice", age: 25 };
for (const key in obj) {
console.log(`Key: ${key}, Value: ${obj[key]}`);
}
// Output:
// Key: name, Value: Alice
// Key: age, Value: 25
❌ 为什么不建议遍历数组?
- 遍历的是索引字符串 :
key是"0","1",而不是数字0,1。 - 可能遍历到原型链属性 :如果有人在
Array.prototype上添加了方法,for...in也会把它遍历出来。
javascript
Array.prototype.customMethod = function () {};
const arr = ["a", "b"];
for (const index in arr) {
console.log(index);
}
// Output: "0", "1", "customMethod" <-- 灾难!
🛡️ 如何安全使用?
如果必须用 for...in 遍历对象,务必加上 hasOwnProperty 检查:
javascript
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 确保是对象自身的属性,而非继承来的
console.log(obj[key]);
}
}
4. ✨ for...of:现代开发的优选
ES6 引入的 for...of 是遍历可迭代对象 的首选。它直接获取值,而不是键或索引。
✅ 基本用法
javascript
const arr = ["a", "b", "c"];
for (const value of arr) {
console.log(value);
}
// Output:
// a
// b
// c
💡 优势
- 语义清晰 :直接拿到值,无需
arr[i]这种间接访问。 - 支持所有可迭代对象 :
ArrayString(遍历字符)Map(遍历[key, value]数组)Set(遍历值)NodeList(DOM 集合)arguments对象
- 支持 break/continue/return:可以中途退出循环。
- 异步友好 :同样支持
async/await串行执行。
javascript
// 遍历 Map
const map = new Map([
["name", "Alice"],
["age", 25],
]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// 遍历 String
for (const char of "Hello") {
console.log(char);
}
⚠️ 局限性
- 不能遍历普通对象 :普通 Object 不是"可迭代"的(没有实现 Iterator 接口)。如果需要遍历对象,请使用
Object.keys()、Object.values()或Object.entries()配合for...of。
javascript
const obj = { name: "Alice", age: 25 };
// ✅ 正确做法
for (const key of Object.keys(obj)) {
console.log(key);
}
for (const [key, value] of Object.entries(obj)) {
console.log(`${key}: ${value}`);
}
5. 🧬 底层原理:迭代器协议 (Iterator Protocol)
要真正理解 for...of,必须理解 Iterator(迭代器)。
🔹 什么是可迭代对象?
一个对象如果要被 for...of 遍历,它必须实现 Iterable Protocol ,即拥有一个名为 Symbol.iterator 的方法,该方法返回一个迭代器对象。
🔹 迭代器对象长什么样?
迭代器对象必须有一个 next() 方法,每次调用返回一个结果对象: { value: any, done: boolean }。
🔹 手动模拟 for...of
javascript
const arr = ["a", "b"];
// 1. 获取迭代器
const iterator = arr[Symbol.iterator]();
// 2. 手动调用 next()
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
for...of 循环在底层其实就是自动调用了这个过程:
- 调用对象的
[Symbol.iterator]()获取迭代器。 - 不断调用
iterator.next()。 - 当
done为true时,停止循环。 - 将每次的
value赋值给循环变量。
关键点 :
普通对象
{}默认没有Symbol.iterator方法,所以不能用for...of。数组、String、Map、Set 等都内置了该方法。
6. 💡 最佳实践总结
| 场景 | 推荐写法 | 理由 |
|---|---|---|
| 遍历数组取值 | for...of |
简洁、安全、直接取值 |
| 遍历数组且需要索引 | for 或 forEach |
for 性能最好;forEach 代码少但不能 break |
| 遍历对象属性 | for...in + hasOwnProperty 或 Object.keys().forEach() |
for...in 需防原型污染;Object.keys 更现代 |
| 需要异步串行执行 | for...of 或 for |
两者都支持 await |
| 需要异步并行执行 | Promise.all() + map |
循环本身无法并行,需结合 Promise API |
| 高性能大数据量遍历 | for |
减少引擎开销,极致性能 |
🚀 博主寄语
- 忘掉
for...in遍历数组的习惯 :那是早期的误区,现在请用for...of或forEach。 for...of是主流 :在现代 JavaScript 开发中,除非你需要索引或极致性能,否则for...of是遍历集合的最佳选择。- 理解迭代器 :当你遇到自定义数据结构需要遍历时,实现
Symbol.iterator接口,就能让它无缝接入for...of、解构赋值和展开运算符...。
记住口诀 :
数组遍历用
of,取值直接又舒服。对象遍历用
in,记得过滤原型链。传统
for最灵活,索引性能两不误。若要遍历普通对象,
keysentries来辅助。异步串行莫惊慌,
of和for都能扛。
希望这篇文档能帮你理清 JavaScript 循环的脉络!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️