For 循环:遍历的艺术
假设你需要给公司的 100 名员工发送新年祝福邮件。你不会手动复制粘贴 100 次,而是会创建一个邮件模板,然后让程序自动遍历员工列表,给每个人发送一封。这种"重复执行同一操作"的场景,正是循环语句大显身手的地方。在 JavaScript 中,for 循环家族是最常用的迭代工具。
传统 For 循环
最经典的 for 循环由三个部分组成:初始化、条件判断和更新表达式。它的结构就像一个精确的时钟机制,按照设定的规则一圈圈地转动:
for (initialization; condition; increment) {
// 循环体
}让我们从一个简单的例子开始,打印 1 到 5 的数字:
for (let i = 1; i <= 5; i++) {
console.log(i);
}
// 输出:
// 1
// 2
// 3
// 4
// 5这个循环的执行过程是这样的:
- 初始化(
let i = 1):在循环开始前执行一次,创建计数器变量i并赋值为 1 - 条件判断(
i <= 5):每次循环前检查,如果为真则执行循环体,否则结束循环 - 执行循环体:输出当前
i的值 - 更新(
i++):每次循环体执行完后,将i加 1 - 返回步骤 2,继续下一次迭代
当 i 变成 6 时,条件 i <= 5 为假,循环结束。
遍历数组
For 循环最常见的用途之一是遍历数组。通过索引访问数组元素,我们可以处理每一项数据:
let fruits = ["apple", "banana", "orange", "grape", "mango"];
for (let i = 0; i < fruits.length; i++) {
console.log(`Fruit ${i + 1}: ${fruits[i]}`);
}
// 输出:
// Fruit 1: apple
// Fruit 2: banana
// Fruit 3: orange
// Fruit 4: grape
// Fruit 5: mango注意这里使用 i < fruits.length 而不是 i <= fruits.length,因为数组索引从 0 开始。如果数组有 5 个元素,有效索引是 0 到 4,使用 <= 会导致访问不存在的索引 5。
灵活的循环控制
For 循环的三个部分都是可选的,这提供了极大的灵活性:
// 在循环外初始化
let i = 0;
for (; i < 5; i++) {
console.log(i);
}
// 在循环体内更新
let j = 0;
for (; j < 5; ) {
console.log(j);
j++;
}
// 所有部分都省略(需要手动break,否则会无限循环)
let k = 0;
for (;;) {
if (k >= 5) break;
console.log(k);
k++;
}虽然这些写法都合法,但标准的三段式循环最清晰,应该优先使用。
递减和步长控制
循环不一定要递增,也可以递减或使用其他步长:
// 倒数
for (let i = 5; i >= 1; i--) {
console.log(`Countdown: ${i}`);
}
// 输出: Countdown: 5, 4, 3, 2, 1
// 每次跳两步
for (let i = 0; i < 10; i += 2) {
console.log(i);
}
// 输出: 0, 2, 4, 6, 8
// 处理数组的偶数索引
let numbers = [10, 20, 30, 40, 50, 60];
for (let i = 0; i < numbers.length; i += 2) {
console.log(numbers[i]);
}
// 输出: 10, 30, 50倒序遍历在某些场景下很有用,比如从数组末尾开始处理,或者在遍历过程中删除元素:
let items = ["a", "b", "c", "d", "e"];
// 从后往前遍历并删除,避免索引混乱
for (let i = items.length - 1; i >= 0; i--) {
if (items[i] === "c") {
items.splice(i, 1);
}
}
console.log(items); // ["a", "b", "d", "e"]嵌套循环
当处理多维数据或需要组合遍历时,可以使用嵌套循环:
// 创建乘法表
for (let i = 1; i <= 3; i++) {
for (let j = 1; j <= 3; j++) {
console.log(`${i} × ${j} = ${i * j}`);
}
console.log("---");
}
// 输出:
// 1 × 1 = 1
// 1 × 2 = 2
// 1 × 3 = 3
// ---
// 2 × 1 = 2
// 2 × 2 = 4
// 2 × 3 = 6
// ---
// 3 × 1 = 3
// 3 × 2 = 6
// 3 × 3 = 9
// ---遍历二维数组:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
console.log(`matrix[${row}][${col}] = ${matrix[row][col]}`);
}
}嵌套循环的时间复杂度会快速增长。两层嵌套是 O(n²),三层是 O(n³)。在处理大数据集时要特别注意性能。
For...of 循环:遍历可迭代对象
ES6 引入了 for...of 循环,专门用于遍历可迭代对象(如数组、字符串、Set、Map 等)。它的语法更简洁,不需要管理索引:
let colors = ["red", "green", "blue"];
for (let color of colors) {
console.log(color);
}
// 输出:
// red
// green
// blue相比传统 for 循环,for...of 更关注"什么"而不是"怎么做"。你不需要写 colors[i],直接获取每个元素。这种声明式的风格更易读,也更不容易出错。
遍历字符串
字符串也是可迭代的,可以逐个字符遍历:
let message = "Hello";
for (let char of message) {
console.log(char);
}
// 输出:
// H
// e
// l
// l
// o这对处理 Unicode 字符特别有用,因为 for...of 能正确识别 Unicode 代码点,而不会把一些特殊字符拆分成多个部分。
遍历 Set 和 Map
// Set
let uniqueNumbers = new Set([1, 2, 3, 2, 1]);
for (let num of uniqueNumbers) {
console.log(num);
}
// 输出: 1, 2, 3
// Map
let userRoles = new Map([
["Alice", "admin"],
["Bob", "editor"],
["Charlie", "viewer"],
]);
for (let [name, role] of userRoles) {
console.log(`${name}: ${role}`);
}
// 输出:
// Alice: admin
// Bob: editor
// Charlie: viewer注意在遍历 Map 时,我们使用了解构赋值 [name, role] 来直接获取键值对。
For...of 的限制
for...of 不能直接用于普通对象,因为普通对象默认不可迭代:
let person = { name: "Alice", age: 30 };
// ❌ 这会报错:person is not iterable
for (let item of person) {
console.log(item);
}
// ✅ 可以遍历对象的值
for (let value of Object.values(person)) {
console.log(value);
}
// 输出: Alice, 30
// ✅ 或遍历键值对
for (let [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`);
}
// 输出:
// name: Alice
// age: 30For...in 循环:遍历对象属性
for...in 循环用于遍历对象的可枚举属性(包括继承的属性):
let car = {
brand: "Tesla",
model: "Model 3",
year: 2023,
color: "white",
};
for (let key in car) {
console.log(`${key}: ${car[key]}`);
}
// 输出:
// brand: Tesla
// model: Model 3
// year: 2023
// color: whiteFor...in 与数组
技术上,for...in 也可以用于数组,但不推荐:
let numbers = [10, 20, 30];
// ❌ 不推荐:for...in 遍历数组
for (let index in numbers) {
console.log(typeof index); // string!
console.log(numbers[index]);
}问题在于:
for...in遍历的索引是字符串类型,不是数字- 它会遍历所有可枚举属性,包括你可能添加到数组上的自定义属性
- 遍历顺序不保证(虽然大多数引擎会按索引顺序)
对于数组,应该使用传统 for 循环、for...of 或数组方法:
let numbers = [10, 20, 30];
// ✅ 推荐方式
for (let num of numbers) {
console.log(num);
}
// 或
numbers.forEach((num) => console.log(num));过滤继承属性
for...in 会遍历对象自身和继承的可枚举属性。如果只想处理自身属性,使用 hasOwnProperty:
let animal = { eats: true };
let rabbit = Object.create(animal);
rabbit.jumps = true;
for (let prop in rabbit) {
console.log(`${prop}: ${rabbit[prop]}`);
}
// 输出:
// jumps: true
// eats: true (继承的)
// 只遍历自身属性
for (let prop in rabbit) {
if (rabbit.hasOwnProperty(prop)) {
console.log(`${prop}: ${rabbit[prop]}`);
}
}
// 输出:
// jumps: true或者更简洁地使用 Object.keys()、Object.values() 或 Object.entries(),它们只返回自身属性:
let rabbit = { jumps: true, color: "white" };
// 只遍历自身属性的键
for (let key of Object.keys(rabbit)) {
console.log(key);
}
// 只遍历自身属性的值
for (let value of Object.values(rabbit)) {
console.log(value);
}
// 遍历键值对
for (let [key, value] of Object.entries(rabbit)) {
console.log(`${key}: ${value}`);
}三种 For 循环的对比
| 特性 | for | for...of | for...in |
|---|---|---|---|
| 主要用途 | 通用循环,精确控制 | 遍历可迭代对象 | 遍历对象属性 |
| 适用于 | 任何场景 | 数组、字符串、Set、Map 等 | 对象 |
| 获取内容 | 需要通过索引访问 | 直接获取元素值 | 获取属性键 |
| 索引/键类型 | 数字 | - | 字符串 |
| 性能 | 最快 | 稍慢 | 较慢 |
| 可读性 | 中等 | 高(简洁) | 高(清晰) |
何时使用:
- for:需要精确控制循环,如自定义步长、复杂条件、性能关键
- for...of:遍历数组、字符串等可迭代对象,关注元素值
- for...in:遍历对象属性
实际应用场景
1. 数据转换
let prices = [19.99, 29.99, 39.99, 49.99];
let pricesWithTax = [];
// 计算含税价格
for (let i = 0; i < prices.length; i++) {
pricesWithTax[i] = prices[i] * 1.1; // 10% 税
}
console.log(pricesWithTax); // [21.989, 32.989, 43.989, 54.989]
// 使用 for...of 更简洁
let pricesWithTax2 = [];
for (let price of prices) {
pricesWithTax2.push(price * 1.1);
}2. 查找元素
let users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
];
function findUserById(id) {
for (let user of users) {
if (user.id === id) {
return user;
}
}
return null; // 未找到
}
console.log(findUserById(2)); // { id: 2, name: "Bob" }
console.log(findUserById(5)); // null3. 累加计算
let scores = [85, 92, 78, 95, 88];
let total = 0;
for (let score of scores) {
total += score;
}
let average = total / scores.length;
console.log(`Average score: ${average}`); // Average score: 87.64. 构建 HTML
let items = ["Home", "About", "Services", "Contact"];
let navHTML = "<nav><ul>";
for (let item of items) {
navHTML += `<li><a href="#${item.toLowerCase()}">${item}</a></li>`;
}
navHTML += "</ul></nav>";
console.log(navHTML);
// <nav><ul><li><a href="#home">Home</a></li><li><a href="#about">About</a></li>...</ul></nav>5. 批量操作
let tasks = [
{ id: 1, title: "Write report", completed: false },
{ id: 2, title: "Review code", completed: false },
{ id: 3, title: "Update docs", completed: false },
];
// 标记所有任务为已完成
for (let task of tasks) {
task.completed = true;
console.log(`Marked "${task.title}" as completed`);
}性能优化技巧
1. 缓存数组长度
在传统 for 循环中,如果循环条件是 i < array.length,每次迭代都会访问 length 属性。对于大数组,可以缓存这个值:
let data = new Array(1000000).fill(0);
// ❌ 每次都访问 length
for (let i = 0; i < data.length; i++) {
// 处理 data[i]
}
// ✅ 缓存 length
let len = data.length;
for (let i = 0; i < len; i++) {
// 处理 data[i]
}不过,现代 JavaScript 引擎通常会优化这种情况,所以性能差异可能很小。可读性往往更重要。
2. 选择合适的循环类型
对于简单的数组遍历,for...of 虽然稍慢,但差异通常可以忽略,而可读性提升是实实在在的。只有在性能关键的代码路径中,才需要考虑使用传统 for 循环。
3. 避免在循环中进行昂贵操作
// ❌ 每次循环都查询 DOM
for (let i = 0; i < 100; i++) {
document.getElementById("output").innerHTML += i + "<br>";
}
// ✅ 先构建字符串,最后一次性更新 DOM
let output = "";
for (let i = 0; i < 100; i++) {
output += i + "<br>";
}
document.getElementById("output").innerHTML = output;常见陷阱
1. 修改正在遍历的数组
let numbers = [1, 2, 3, 4, 5];
// ❌ 危险:在遍历时删除元素
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
numbers.splice(i, 1); // 删除偶数
}
}
console.log(numbers); // [1, 3, 5] 但可能跳过某些元素
// ✅ 从后往前遍历
let numbers2 = [1, 2, 3, 4, 5];
for (let i = numbers2.length - 1; i >= 0; i--) {
if (numbers2[i] % 2 === 0) {
numbers2.splice(i, 1);
}
}
console.log(numbers2); // [1, 3, 5]2. 闭包陷阱(var vs let)
// ❌ 使用 var
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i); // 输出 3, 3, 3
}, 100);
}
// ✅ 使用 let
for (let j = 0; j < 3; j++) {
setTimeout(function () {
console.log(j); // 输出 0, 1, 2
}, 100);
}使用 var 时,所有迭代共享同一个 i 变量。使用 let 时,每次迭代都有自己的 j 副本。
3. For...in 遍历数组的陷阱
let arr = ["a", "b", "c"];
arr.customProp = "custom";
// ❌ for...in 会遍历自定义属性
for (let index in arr) {
console.log(arr[index]);
}
// 输出: a, b, c, custom
// ✅ for...of 只遍历数组元素
for (let item of arr) {
console.log(item);
}
// 输出: a, b, c总结
For 循环是 JavaScript 中不可或缺的工具,掌握它们能让你高效地处理各种迭代场景。传统 for 循环提供最大的灵活性和控制,for...of 让遍历可迭代对象变得简洁优雅,for...in 则是遍历对象属性的利器。
关键要点:
- 传统
for循环适合需要精确控制的场景 for...of是遍历数组、字符串等的首选,语法简洁for...in用于遍历对象属性,不推荐用于数组- 嵌套循环要注意时间复杂度
- 避免在循环中修改正在遍历的数组
- 使用
let而不是var来避免闭包问题 - 性能优化应该在确认瓶颈后进行,可读性同样重要
选择合适的循环类型,能让你的代码既高效又易读。随着实践的增多,你会越来越熟练地运用这些工具。