Skip to content

For 循环:遍历的艺术

假设你需要给公司的 100 名员工发送新年祝福邮件。你不会手动复制粘贴 100 次,而是会创建一个邮件模板,然后让程序自动遍历员工列表,给每个人发送一封。这种"重复执行同一操作"的场景,正是循环语句大显身手的地方。在 JavaScript 中,for 循环家族是最常用的迭代工具。

传统 For 循环

最经典的 for 循环由三个部分组成:初始化、条件判断和更新表达式。它的结构就像一个精确的时钟机制,按照设定的规则一圈圈地转动:

javascript
for (initialization; condition; increment) {
  // 循环体
}

让我们从一个简单的例子开始,打印 1 到 5 的数字:

javascript
for (let i = 1; i <= 5; i++) {
  console.log(i);
}

// 输出:
// 1
// 2
// 3
// 4
// 5

这个循环的执行过程是这样的:

  1. 初始化let i = 1):在循环开始前执行一次,创建计数器变量 i 并赋值为 1
  2. 条件判断i <= 5):每次循环前检查,如果为真则执行循环体,否则结束循环
  3. 执行循环体:输出当前 i 的值
  4. 更新i++):每次循环体执行完后,将 i 加 1
  5. 返回步骤 2,继续下一次迭代

i 变成 6 时,条件 i <= 5 为假,循环结束。

遍历数组

For 循环最常见的用途之一是遍历数组。通过索引访问数组元素,我们可以处理每一项数据:

javascript
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 循环的三个部分都是可选的,这提供了极大的灵活性:

javascript
// 在循环外初始化
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++;
}

虽然这些写法都合法,但标准的三段式循环最清晰,应该优先使用。

递减和步长控制

循环不一定要递增,也可以递减或使用其他步长:

javascript
// 倒数
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

倒序遍历在某些场景下很有用,比如从数组末尾开始处理,或者在遍历过程中删除元素:

javascript
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"]

嵌套循环

当处理多维数据或需要组合遍历时,可以使用嵌套循环:

javascript
// 创建乘法表
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
// ---

遍历二维数组:

javascript
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 等)。它的语法更简洁,不需要管理索引:

javascript
let colors = ["red", "green", "blue"];

for (let color of colors) {
  console.log(color);
}

// 输出:
// red
// green
// blue

相比传统 for 循环,for...of 更关注"什么"而不是"怎么做"。你不需要写 colors[i],直接获取每个元素。这种声明式的风格更易读,也更不容易出错。

遍历字符串

字符串也是可迭代的,可以逐个字符遍历:

javascript
let message = "Hello";

for (let char of message) {
  console.log(char);
}

// 输出:
// H
// e
// l
// l
// o

这对处理 Unicode 字符特别有用,因为 for...of 能正确识别 Unicode 代码点,而不会把一些特殊字符拆分成多个部分。

遍历 Set 和 Map

javascript
// 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 不能直接用于普通对象,因为普通对象默认不可迭代:

javascript
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: 30

For...in 循环:遍历对象属性

for...in 循环用于遍历对象的可枚举属性(包括继承的属性):

javascript
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: white

For...in 与数组

技术上,for...in 也可以用于数组,但不推荐

javascript
let numbers = [10, 20, 30];

// ❌ 不推荐:for...in 遍历数组
for (let index in numbers) {
  console.log(typeof index); // string!
  console.log(numbers[index]);
}

问题在于:

  1. for...in 遍历的索引是字符串类型,不是数字
  2. 它会遍历所有可枚举属性,包括你可能添加到数组上的自定义属性
  3. 遍历顺序不保证(虽然大多数引擎会按索引顺序)

对于数组,应该使用传统 for 循环、for...of 或数组方法:

javascript
let numbers = [10, 20, 30];

// ✅ 推荐方式
for (let num of numbers) {
  console.log(num);
}

// 或
numbers.forEach((num) => console.log(num));

过滤继承属性

for...in 会遍历对象自身和继承的可枚举属性。如果只想处理自身属性,使用 hasOwnProperty

javascript
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(),它们只返回自身属性:

javascript
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 循环的对比

特性forfor...offor...in
主要用途通用循环,精确控制遍历可迭代对象遍历对象属性
适用于任何场景数组、字符串、Set、Map 等对象
获取内容需要通过索引访问直接获取元素值获取属性键
索引/键类型数字-字符串
性能最快稍慢较慢
可读性中等高(简洁)高(清晰)

何时使用:

  • for:需要精确控制循环,如自定义步长、复杂条件、性能关键
  • for...of:遍历数组、字符串等可迭代对象,关注元素值
  • for...in:遍历对象属性

实际应用场景

1. 数据转换

javascript
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. 查找元素

javascript
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)); // null

3. 累加计算

javascript
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.6

4. 构建 HTML

javascript
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. 批量操作

javascript
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 属性。对于大数组,可以缓存这个值:

javascript
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. 避免在循环中进行昂贵操作

javascript
// ❌ 每次循环都查询 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. 修改正在遍历的数组

javascript
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)

javascript
// ❌ 使用 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 遍历数组的陷阱

javascript
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 来避免闭包问题
  • 性能优化应该在确认瓶颈后进行,可读性同样重要

选择合适的循环类型,能让你的代码既高效又易读。随着实践的增多,你会越来越熟练地运用这些工具。