JavaScript 遍历方法详解

121 阅读11分钟

JavaScript 遍历方法详解

JavaScript 提供了多种遍历方法,每种方法都有其独特的语法结构、使用场景和注意事项。掌握这些遍历方法不仅能提高代码质量,还能使开发更加高效。本文将系统地介绍 JavaScript 中常见的遍历方法,包括对象遍历和数组遍历两大类,并分析它们的特点、适用场景及最佳实践。

对象遍历方法

for...in 循环

语法

for (const property in object) {
  // 使用object[property]访问属性值
}

用法:for...in 循环用于遍历对象的所有可枚举属性,包括继承的属性。它会按顺序返回对象自身的所有可枚举属性,以及原型链上可枚举的属性,直到到达原型链的终点。

const person = {
  name: 'Alice',
  age: 25,
  occupation: 'Engineer'
};

for (const key in person) {
  console.log(`${key}: ${person[key]}`);
}
// 输出: name: Alice, age: 25, occupation: Engineer

注意事项

  • 遍历继承属性:for...in 会遍历对象原型链上的属性,可能导致意外结果。

  • 使用 hasOwnProperty 过滤:若只需遍历对象自身的属性,应使用 hasOwnProperty 方法过滤。

    for (const key in person) {
      if (person.hasOwnProperty(key)) {
        console.log(`${key}: ${person[key]}`);
      }
    }
    
  • 不处理 Symbol 类型属性:for...in 无法遍历 Symbol 类型的属性。

  • 不可枚举属性不被访问:即使属性不可枚举,for...in 也不会遍历它们。

  • 迭代过程中修改对象可能有问题:在循环过程中添加、删除或修改对象属性可能导致不可预测的行为。

Object.keys() + for...of/forEach

语法

// 结合for...of
for (const key of Object.keys(object)) {
  // 使用object[key]访问属性值
}

// 结合forEach
Object.keys(object).forEach(key => {
  // 使用object[key]访问属性值
});

用法:Object.keys () 返回对象所有可枚举的自有属性名组成的数组,结合 for...of 或 forEach 可安全遍历对象自身属性。

const person = {
  name: 'Alice',
  age: 25,
  occupation: 'Engineer'
};

// for...of结合
for (const key of Object.keys(person)) {
  console.log(`${key}: ${person[key]}`);
}
// 输出: name: Alice, age: 25, occupation: Engineer

// forEach结合
Object.keys(person).forEach(key => {
  console.log(person[key]);
});
// 同样输出三个属性值

注意事项

  • 仅遍历自有属性:与 for...in 不同,Object.keys () 仅遍历对象自身的可枚举属性。
  • 不包含 Symbol 键:Object.keys () 仅返回字符串类型的属性名。
  • ESLint 推荐:许多 JavaScript 风格指南推荐使用 Object.keys () 代替 for...in 遍历数组。
  • 返回值是数组:Object.keys () 返回的是数组,可以像其他数组一样进行操作(如排序、过滤)。

Object.values() + for...of/forEach

语法

// 结合for...of
for (const value of Object.values(object)) {
  // 直接使用value
}

// 结合forEach
Object.values(object).forEach(value => {
  // 直接使用value
});

用法:Object.values () 返回对象所有可枚举的自有属性值组成的数组,结合 for...of 或 forEach 可直接遍历对象属性值。

const person = {
  name: 'Alice',
  age: 25,
  occupation: 'Engineer'
};

// for...of结合
for (const value of Object.values(person)) {
  console.log(value);
}
// 输出: Alice, 25, Engineer

// forEach结合
Object.values(person).forEach(value => {
  console.log(value);
});
// 同样输出三个属性值

注意事项

  • 仅遍历自有属性值:与 Object.keys () 类似,Object.values () 也仅遍历对象自身的可枚举属性。
  • 不包含键信息:无法直接获取属性名,只能访问属性值(若需键名需使用 Object.entries ())。
  • ESLint 推荐:当只需要属性值时,使用 Object.values () 比 for...in 更高效、更安全。
  • 返回值是数组:Object.values () 返回的是数组,支持数组的所有方法(如 map、filter)。

Object.entries() + for...of/forEach

语法

// 结合for...of
for (const [key, value] of Object.entries(object)) {
  // 同时使用key和value
}

// 结合forEach
Object.entries(object).forEach(([key, value]) => {
  // 同时使用key和value
});

用法:Object.entries () 返回对象所有可枚举的自有属性的键值对数组,结合 for...of 或 forEach 可同时访问属性名和属性值。

const person = {
  name: 'Alice',
  age: 25,
  occupation: 'Engineer'
};

// for...of结合
for (const [key, value] of Object.entries(person)) {
  console.log(`Key: ${key}, Value: ${value}`);
}
// 输出: Key: name, Value: Alice; Key: age, Value: 25; Key: occupation, Value: Engineer

// forEach结合
Object.entries(person).forEach(([key, value]) => {
  console.log(`Key: ${key}, Value: ${value}`);
});
// 同样输出三个键值对

注意事项

  • 仅遍历自有属性:与 Object.keys () 和 Object.values () 一样,Object.entries () 也仅遍历对象自身的可枚举属性。
  • 返回键值对数组:每个键值对以数组形式返回,索引 0 为键,索引 1 为值,支持数组解构赋值。
  • ESLint 推荐:当需要同时访问键和值时,Object.entries () 是比 for...in 更安全的选择。
  • 返回值是数组:Object.entries () 返回的是数组,可结合数组方法进行复杂操作。

Reflect.ownKeys()

语法

const keys = Reflect.ownKeys(object);

用法:Reflect.ownKeys () 返回对象所有自有属性(包括不可枚举属性和 Symbol 类型的属性)的键集合,是遍历对象所有属性的最全面方法。

const person = {
  name: 'Alice',
  age: 25,
  occupation: 'Engineer'
};

// 添加不可枚举属性
Object.defineProperty(person, 'id', {
  value: 1001,
  enumerable: false // 不可枚举
});

// 添加Symbol类型属性
person[Symbol('secret')] = 'abc123';

const allKeys = Reflect.ownKeys(person);
console.log(allKeys); 
// 输出: ['name', 'age', 'occupation', 'id', Symbol(secret)]

注意事项

  • 包含所有自有属性:包括可枚举和不可枚举的属性,不受 enumerable 属性影响。
  • 支持 Symbol 类型的键:与 Object.keys () 不同,Reflect.ownKeys () 可以返回 Symbol 类型的键。
  • 不遍历继承属性:仅遍历对象自身的属性,不包含原型链上的属性。
  • 返回数组:返回一个包含所有自有属性键的数组,可以像其他数组一样进行操作。
  • 性能考量:相比 Object.keys (),Reflect.ownKeys () 可能稍慢,因为需要处理更多类型的键(不可枚举、Symbol)。

其他自有属性方法

JavaScript 还提供了其他几种遍历对象自有属性的方法,适用于特定场景:

1. Object.getOwnPropertyNames()
  • 语法Object.getOwnPropertyNames(object)

  • 用法:返回对象所有自有属性名(包括不可枚举的)的数组,但不包含 Symbol 类型的键。

  • 示例

    const person = {
      name: 'Alice',
      age: 25
    };
    Object.defineProperty(person, 'id', { value: 1001, enumerable: false });
    
    const keys = Object.getOwnPropertyNames(person);
    console.log(keys); // 输出: ['name', 'age', 'id']
    
2. Object.getOwnPropertySymbols()
  • 语法Object.getOwnPropertySymbols(object)

  • 用法:返回对象所有自有 Symbol 类型属性名的数组,仅包含 Symbol 类型的键。

  • 示例

    const person = {
      name: 'Alice',
      [Symbol('secret')]: 'abc123'
    };
    
    const symbols = Object.getOwnPropertySymbols(person);
    console.log(symbols); // 输出: [Symbol(secret)]
    
3. for...in 循环 + hasOwnProperty
  • 语法

    for (const key in object) {
      if (object.hasOwnProperty(key)) {
        // 处理属性
      }
    }
    
  • 用法:通过 hasOwnProperty 过滤继承属性,仅遍历对象自身的属性,是 for...in 的安全用法。

  • 示例

    const person = {
      name: 'Alice',
      age: 25
    };
    
    // 给原型添加属性(继承属性)
    Object.prototype.gender = 'female';
    
    for (const key in person) {
      if (person.hasOwnProperty(key)) {
        console.log(`${key}: ${person[key]}`);
      }
    }
    // 输出: name: Alice, age: 25(过滤了gender属性)
    

注意事项

  • 性能差异:Object.getOwnPropertyNames () 和 Object.getOwnPropertySymbols () 相比 Object.keys () 会返回更多属性,但性能略低。
  • 适用场景:当需要遍历所有自有属性(包括不可枚举的)时,使用这些方法;当仅需可枚举属性时,优先使用 Object.keys ()/values ()/entries ()。
  • Symbol 键的特殊性:Symbol 类型的键不会被 for...in、Object.keys () 等方法捕获,需使用 Reflect.ownKeys () 或 Object.getOwnPropertySymbols () 专门处理。

数组遍历方法

传统 for 循环

语法

for (let i = 0; i < array.length; i++) {
  // 使用array[i]访问元素
}

用法:传统 for 循环是最基础的数组遍历方式,通过索引控制循环流程,支持灵活的循环控制(break、continue、return)。

const numbers = [1, 2, 3, 4, 5];

// 遍历数组并打印元素
for (let i = 0; i < numbers.length; i++) {
  console.log(`Index ${i}: ${numbers[i]}`);
}
// 输出: Index 0: 1; Index 1: 2; ...; Index 4: 5

// 遍历数组并修改元素
for (let i = 0; i < numbers.length; i++) {
  numbers[i] = numbers[i] * 2;
}
console.log(numbers); // 输出: [2, 4, 6, 8, 10]

// 中断循环(找到第一个偶数)
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    console.log(`第一个偶数: ${numbers[i]}`);
    break;
  }
}

注意事项

  • 完全控制循环:支持 break(中断循环)、continue(跳过当前迭代)、return(退出函数)等控制语句。

  • 可修改原数组:通过索引直接访问元素,可直接修改原数组的值。

  • 性能最佳:在大型数组遍历中,传统 for 循环的性能通常优于 forEach、map 等方法(减少函数调用开销)。

  • 索引管理:需要手动管理索引变量(i 的初始化、条件判断、递增),容易出现索引错误(如数组越界)。

  • 缓存数组长度:对于长度固定的数组,可缓存 length 属性以提高性能:

    const len = numbers.length;
    for (let i = 0; i < len; i++) {
      // 处理逻辑
    }
    

for...of 循环

语法

for (const element of array) {
  // 直接使用element
}

用法:for...of 循环是 ES6 引入的遍历可迭代对象(数组、字符串、Map、Set 等)的方法,直接遍历元素值,无需索引。

const numbers = [1, 2, 3, 4, 5];

// 遍历数组元素
for (const num of numbers) {
  console.log(num);
}
// 输出: 1, 2, 3, 4, 5

// 遍历数组并获取索引(结合Array.prototype.entries())
for (const [index, num] of numbers.entries()) {
  console.log(`Index ${index}: ${num}`);
}
// 输出: Index 0: 1; Index 1: 2; ...; Index 4: 5

// 遍历字符串
const str = 'hello';
for (const char of str) {
  console.log(char);
}
// 输出: h, e, l, l, o

注意事项

  • 直接遍历元素:无需手动管理索引,直接访问元素值,代码更简洁。
  • 支持可迭代对象:除数组外,还支持遍历字符串、Map、Set、Generator 等可迭代对象。
  • 可中断循环:支持 break、continue、return 等控制语句,可提前终止循环。
  • 不遍历非数字属性:与 for...in 不同,for...of 仅遍历数组的数字索引属性,不会遍历非数字属性(如数组的自定义属性)。
  • 不支持修改数组长度:在循环中修改数组长度可能导致遍历不完整或重复遍历(建议避免)。

forEach () 方法

语法

array.forEach((currentValue, [index], [array]) => {
  // 处理逻辑
}, [thisArg]);

参数说明

  • currentValue:当前遍历的元素。
  • index(可选):当前元素的索引。
  • array(可选):被遍历的原数组。
  • thisArg(可选):回调函数中 this 的指向对象。

用法:forEach () 方法对数组中的每个元素执行一次回调函数,无返回值,仅用于遍历执行操作。

const numbers = [1, 2, 3, 4, 5];

// 基础用法
numbers.forEach(num => {
  console.log(num);
});
// 输出: 1, 2, 3, 4, 5

// 带索引参数
numbers.forEach((num, index) => {
  console.log(`Index ${index}: Value ${num}`);
});
// 输出: Index 0: Value 1; Index 1: Value 2; ...; Index 4: Value 5

// 使用thisArg参数
const obj = { multiplier: 2 };
numbers.forEach(function(num) {
  console.log(num * this.multiplier); // this指向obj
}, obj);
// 输出: 2, 4, 6, 8, 10

注意事项

  • 无返回值:forEach () 不返回新数组,仅执行操作(若需返回结果,应使用 map、filter 等方法)。

  • 无法中断循环:不支持 break 和 continue,无法提前终止循环(即使抛出异常也不推荐)。

  • 回调函数特性

    • 若使用普通函数作为回调,this 值由 thisArg 参数指定;
    • 若使用箭头函数,thisArg 参数无效,this 指向外层作用域的 this。
  • 不改变原数组:forEach () 本身不会修改原数组,但若在回调函数中显式修改元素(如array[index] = num * 2),则会改变原数组。

  • ESLint 警告:某些 ESLint 配置(如no-foreach)会警告使用 forEach,因为它可能隐藏副作用(建议优先使用函数式方法)。

map () 方法

语法

const newArray = array.map((currentValue, [index], [array]) => {
  // 处理逻辑,返回新值
}, [thisArg]);

用法:map () 方法对数组中的每个元素执行回调函数,将回调函数的返回值组成新数组返回,原数组保持不变。适用于数组元素的转换操作。

const numbers = [1, 2, 3, 4, 5];

// 基础转换(数值翻倍)
const doubled = numbers.map(num => num * 2);
console.log(doubled); // 输出: [2, 4, 6, 8, 10]
console.log(numbers); // 原数组不变: [1, 2, 3, 4, 5]

// 转换为对象数组
const objects = numbers.map((num, index) => ({
  id: index,
  value: num,
  squared: num * num
}));
console.log(objects);
// 输出: [
//   { id: 0, value: 1, squared: 1 },
//   { id: 1, value: 2, squared: 4 },
//   ...
// ]

// 字符串处理
const names = ['alice', 'bob', 'charlie'];
const capitalized = names.map(name => name.charAt(0).toUpperCase() + name.slice(1));
console.log(capitalized); // 输出: ['Alice', 'Bob', 'Charlie']

注意事项

  • 返回新数组:始终返回与原数组长度相同的新数组(即使回调函数返回 undefined),原数组不变。

  • 不可中断循环:不支持 break 和 continue,必须遍历完所有元素。

  • 函数式编程:鼓励使用无副作用的纯函数(回调函数不修改外部变量或原数组)。

  • 性能考量:创建新数组可能带来额外内存开销,对于大型数组(百万级元素)需谨慎使用。

  • 常见错误

    • 忘记返回值:回调函数未返回值时,新数组元素为 undefined;
    • 误用索引:将 index 作为元素值使用(如map(index => index * 2));
    • 副作用操作:在回调函数中修改外部变量或原数组(违反纯函数原则)。

filter () 方法

语法

const newArray = array.filter((currentValue, [index], [array]) => {
  // 返回布尔值,决定元素是否保留
}, [thisArg]);

用法:filter () 方法通过回调函数(布尔函数)过滤数组元素,返回由满足条件(回调函数返回 true)的元素组成的新数组,原数组保持不变。适用于数组元素的筛选操作。

const numbers = [1, 2, 3, 4, 5, 6];

// 筛选偶数
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 输出: [2, 4, 6]

// 筛选长度>=5的字符串
const words = ['apple', 'banana', 'kiwi', 'grape', 'orange'];
const longWords = words.filter(word => word.length >= 5);
console.log(longWords); // 输出: ['apple', 'banana', 'orange']

// 筛选对象数组(年龄>=25)
const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 20 }
];
const adults = people.filter(person => person.age >= 25);
console.log(adults); // 输出: [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]

注意事项

  • 返回新数组:返回的新数组长度可能小于或等于原数组(取决于满足条件的元素数量),原数组不变。
  • 不可中断循环:不支持 break 和 continue,必须遍历完所有元素。
  • 布尔返回值:回调函数必须返回布尔值(true/false),非布尔值会被自动转换(如 0→false、非 0→true)。
  • 函数式编程:鼓励使用无副作用的纯函数,回调函数不应修改外部变量或原数组。
  • 性能考量:创建新数组可能带来额外内存开销,对于大型数组需结合实际场景优化。

reduce () 方法

语法

const result = array.reduce((accumulator, currentValue, [index], [array]) => {
  // 累积逻辑,返回新的累积值
}, [initialValue]);

参数说明

  • accumulator:累加器,存储上一次回调函数的返回值(初始值为 initialValue 或数组第一个元素)。
  • currentValue:当前遍历的元素。
  • index(可选):当前元素的索引。
  • array(可选):被遍历的原数组。
  • initialValue(可选):累加器的初始值,若未提供则使用数组第一个元素作为初始值,且从第二个元素开始遍历。

用法:reduce () 方法对数组中的每个元素执行回调函数,将其结果汇总为单个值返回(如求和、求积、对象聚合等),功能强大且灵活。

const numbers = [1, 2, 3, 4, 5];

// 1. 求和(提供初始值)
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 输出: 15

// 2. 求积(未提供初始值)
const product = numbers.reduce((acc, num) => acc * num);
console.log(product); // 输出: 120(1*2*3*4*5)

// 3. 聚合对象(统计年龄总和)
const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 20 }
];
const ageSum = people.reduce((acc, person) => acc + person.age, 0);
console.log(ageSum); // 输出: 75

// 4. 分组统计(按年龄分组)
const ageGroups = people.reduce((acc, person) => {
  const key = person.age >= 25 ? 'adults' : 'youngsters';
  acc[key] = acc[key] ? [...acc[key], person] : [person];
  return acc;
}, {});
console.log(ageGroups);
// 输出: {
//   adults: [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }],
//   youngsters: [{ name: 'Charlie', age: 20 }]
// }

// 5. 数组扁平化(二维数组转一维)
const nestedArray = [[1, 2], [3, 4], [5, 6]];
const flattened = nestedArray.reduce((acc, arr) => [...acc, ...arr], []);
console.log(flattened); // 输出: [1, 2, 3, 4, 5, 6]

注意事项

  • 返回单个值:最终返回一个汇总值(可以是数字、对象、数组等),而非数组。

  • 初始值影响

    • 提供初始值:累加器从初始值开始,遍历所有元素;
    • 未提供初始值:累加器从数组第一个元素开始,遍历从第二个元素开始;
    • 空数组无初始值:会抛出 TypeError(必须提供初始值)。
  • 不可中断循环:不支持 break 和 continue,必须遍历完所有元素。

  • 函数式编程:鼓励使用无副作用的纯函数,每次迭代应返回新的累加器(而非修改原累加器),确保可预测性。

  • 性能考量:在大型数组中可能性能略低,但通常与 forEach、map 等方法差异不大,且功能更强大。

遍历方法对比与选择指南

对象遍历方法对比

方法遍历继承属性处理 Symbol 键返回值类型性能适用场景
for...in属性名字符串(逐个返回)中等遍历对象所有可枚举属性(包括继承),调试场景
Object.keys()可枚举自有属性名数组遍历对象自身可枚举属性名
Object.values()可枚举自有属性值数组遍历对象自身可枚举属性值
Object.entries()可枚举自有属性键值对数组中等同时遍历对象自身可枚举属性的键和值
Reflect.ownKeys()所有自有属性键数组中等遍历对象所有自有属性(包括不可枚举、Symbol)
Object.getOwnPropertyNames()所有自有属性名数组中等遍历对象所有自有属性名(包括不可枚举)
Object.getOwnPropertySymbols()所有自有 Symbol 属性名数组中等遍历对象所有自有 Symbol 属性名

数组遍历方法对比

方法返回值类型可中断循环性能函数式特性适用场景
传统 for 循环需要精确控制索引、修改数组、中断循环
for...of中等遍历可迭代对象(数组、字符串等),无需索引
forEach()中等遍历数组执行操作,无需返回结果
map()新数组(转换后)中等数组元素转换,生成新数组
filter()新数组(筛选后)中等数组元素筛选,生成新数组
reduce()单个汇总值中等数组元素累积计算(求和、分组、扁平化等)

适用场景选择建议

1. 对象遍历场景
  • 调试对象属性:使用for...in(快速查看所有可枚举属性,包括继承)。

  • 安全遍历自身可枚举属性

    • 仅需键名:Object.keys() + for...of/forEach;
    • 仅需值:Object.values() + for...of/forEach;
    • 需键值对:Object.entries() + for...of/forEach(ESLint 推荐)。
  • 遍历所有自有属性(包括不可枚举)Reflect.ownKeys() 或 Object.getOwnPropertyNames()

  • 处理 Symbol 类型属性Reflect.ownKeys() 或 Object.getOwnPropertySymbols()

  • 避免继承属性干扰:坚决避免使用for...in,优先使用Object.keys()/values()/entries()。

2. 数组遍历场景
  • 需要索引或修改数组:使用传统 for 循环(性能最佳,控制灵活)。

  • 仅需遍历元素执行操作

    • 无需中断循环:forEach()(代码简洁);
    • 可能需要中断循环:for...of(支持 break/continue)。
  • 转换元素生成新数组map()(一对一转换,保持长度不变)。

  • 筛选元素生成新数组filter()(按条件筛选,长度可能变化)。

  • 累积计算(求和、分组等)reduce()(功能强大,支持复杂聚合)。

  • 遍历可迭代对象(字符串、Map 等)for...of(通用遍历方案)。

3. 函数式编程场景
  • 优先使用mapfilterreduce等高阶函数,代码更简洁、声明式,可维护性更高。
  • 鼓励使用纯函数(无副作用),避免在回调函数中修改外部变量或原数组。
  • 复杂逻辑可组合使用高阶函数(如map().filter().reduce()),替代多层 for 循环。

ESLint 规范与最佳实践

ESLint 对遍历方法的建议

ESLint 作为前端常用的代码检查工具,对遍历方法有明确的规范建议,旨在提高代码质量和一致性:

  1. 禁止使用 for...in 遍历数组(规则:no-for-in

    • 原因:for...in 会遍历数组的非数字属性(如自定义属性、原型链属性),导致意外结果。
    • 解决方案:使用for...offorEach、传统 for 循环或Object.keys()替代。
  2. 优先使用安全的对象遍历方法(规则:prefer-object-spreadprefer-destructuring

    • 原因:for...in可能遍历继承属性,存在安全风险。

    • 解决方案:使用Object.keys()/values()/entries()结合解构赋值,如:

      // 推荐
      for (const [key, value] of Object.entries(obj)) {
        // 处理逻辑
      }
      
      // 不推荐
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          // 处理逻辑
        }
      }
      
  3. 禁止在循环中修改数组(规则:no-unsafe-optional-chainingno-param-reassign

    • 原因:在forEachfor...of等循环中修改数组长度或元素,可能导致遍历不完整或重复遍历。
    • 解决方案:使用mapfilter等高阶函数创建新数组,而非修改原数组。
  4. 优先使用函数式方法替代 forEach(规则:prefer-mapprefer-filterprefer-reduce

    • 原因:forEach可能隐藏副作用,且无法返回结果,函数式方法更具表达力。

    • 解决方案:

      // 不推荐
      const result = [];
      arr.forEach(num => {
        if (num % 2 === 0) {
          result.push(num * 2);
        }
      });
      
      // 推荐
      const result = arr.filter(num => num % 2 === 0).map(num => num * 2);
      

函数式编程最佳实践

  1. 使用纯函数

    • 回调函数不应修改外部变量或原数组,仅依赖输入参数返回结果。

    • 示例:

      // 纯函数(推荐)
      const double = num => num * 2;
      const doubled = [1, 2, 3].map(double);
      
      // 非纯函数(不推荐)
      let total = 0;
      [1, 2, 3].forEach(num => {
        total += num; // 修改外部变量
      });
      
  2. 避免回调地狱

    • 多层forEach嵌套会降低代码可读性,可使用mapfilterreduce组合替代。

    • 示例:

      // 不推荐(嵌套forEach)
      const data = [
        { id: 1, items: [10, 20] },
        { id: 2, items: [30, 40] }
      ];
      const result = [];
      data.forEach(item => {
        item.items.forEach(num => {
          result.push(num * 2);
        });
      });
      
      // 推荐(reduce + map)
      const result = data.reduce((acc, item) => {
        return [...acc, ...item.items.map(num => num * 2)];
      }, []);
      
  3. 合理使用解构赋值

    • 遍历对象键值对时,使用解构赋值简化代码。

    • 示例:

      Object.entries(obj).forEach(([key, value]) => {
        console.log(`${key}: ${value}`);
      });
      
  4. 处理边界情况

    • 数组为空时,reduce必须提供初始值,避免抛出错误。

    • 示例:

      // 推荐(提供初始值)
      const sum = [].reduce((acc, num) => acc + num, 0); // 0
      
      // 不推荐(无初始值,空数组会报错)
      const sum = [].reduce((acc, num) => acc + num); // TypeError
      
  5. 性能优化

    • 大型数组(百万级元素)遍历优先使用传统 for 循环(减少函数调用开销)。
    • 避免在回调函数中执行复杂操作,可提前提取逻辑或缓存中间结果。

常见问题与解决方案

1. for...in 遍历数组时的问题

问题:使用 for...in 遍历数组时,会遍历到数组的非数字属性(如自定义属性、原型链属性),导致意外结果。

const arr = [1, 2, 3];
arr.test = 'value'; // 添加非数字属性
Object.prototype.gender = 'female'; // 原型链添加属性

for (const key in arr) {
  console.log(key); // 输出: 0, 1, 2, 'test', 'gender'
}

解决方案

  • 使用for...of遍历数组(仅遍历数字索引属性):

    for (const num of arr) {
      console.log(num); // 输出: 1, 2, 3
    }
    
  • 使用Object.keys()过滤数字索引:

    Object.keys(arr).forEach(key => {
      if (!isNaN(Number(key))) { // 仅处理数字索引
        console.log(arr[key]); // 输出: 1, 2, 3
      }
    });
    
  • 避免给数组添加非数字属性(遵循数组的设计初衷)。

2. reduce () 方法的初始值问题

问题:reduce () 方法在数组为空或未提供初始值时,行为不符合预期。


// 问题1:空数组无初始值 → 抛出TypeError
[].reduce((acc, num) => acc + num); // Uncaught TypeError: Reduce of empty array with no initial value

// 问题2:数组只有一个元素无初始值 → 直接返回该元素,不调用回调
[5].reduce((acc, num) => acc + num); // 5(回调未执行)

解决方案

  • 始终提供初始值(推荐):

    const sum = [1, 2, 3].reduce((acc, num) => acc + num, 0); // 6
    const emptySum = [].reduce((acc, num) => acc + num, 0); // 0(无错误)
    
  • 明确处理边界情况(数组可能为空时):

    const array = [];
    const sum = array.length === 0 ? 0 : array.reduce((acc, num) => acc + num);
    

3. Symbol 键的遍历问题

问题:Symbol 类型的键无法被for...inObject.keys()等方法捕获,导致遍历不完整。

const obj = {
  name: 'Alice',
  [Symbol('id')]: 123,
  [Symbol('secret')]: 'abc'
};

console.log(Object.keys(obj)); // 输出: ['name'](未包含Symbol键)
for (const key in obj) {
  console.log(key); // 输出: 'name'(未包含Symbol键)
}

解决方案

  • 使用Reflect.ownKeys()遍历所有自有属性键(包括 Symbol):

    const allKeys = Reflect.ownKeys(obj);
    console.log(allKeys); // 输出: ['name', Symbol(id), Symbol(secret)]
    
  • 使用Object.getOwnPropertySymbols()专门获取 Symbol 键:

    const symbols = Object.getOwnPropertySymbols(obj);
    console.log(symbols); // 输出: [Symbol(id), Symbol(secret)]
    

4. 高阶函数的回调参数误用

问题:map、filter 等高阶函数的回调参数误用(如混淆参数顺序、遗漏参数),导致意外结果。


// 问题1:map回调参数顺序错误(误将index作为value)
const numbers = ['1', '2', '3'];
const parsed = numbers.map((index, value) => parseInt(value)); 
// 输出: [NaN, NaN, NaN](参数顺序颠倒)

// 问题2:filter回调未返回布尔值
const evenNumbers = [1, 2, 3, 4].filter(num => {
  num % 2 === 0; // 遗漏return,默认返回undefined → 转换为false
});
console.log(evenNumbers); // 输出: [](所有元素都被过滤)

解决方案

  • 明确回调函数参数顺序:

    • map/filter 回调:(currentValue, index, array)
    • reduce 回调:(accumulator, currentValue, index, array)
  • 确保 filter 回调返回布尔值:

    // 正确示例
    const parsed = numbers.map((value) => parseInt(value)); // 输出: [1, 2, 3]
    const evenNumbers = [1, 2, 3, 4].filter(num => num % 2 === 0); // 输出: [2, 4]
    

5. 遍历过程中修改数组的问题

问题:在forEachfor...of等循环中修改数组(如删除、添加元素),导致遍历不完整或重复遍历。

// 问题:删除元素后,数组长度变化,导致某些元素被跳过
const arr = [1, 2, 3, 4, 5];
arr.forEach((num, index) => {
  if (num % 2 === 0) {
    arr.splice(index, 1); // 删除当前元素
  }
});
console.log(arr); // 输出: [1, 3, 5]?实际输出: [1, 3, 5](看似正确,但逻辑有风险)

// 问题升级:数组长度变化导致遍历异常
const arr2 = [1, 2, 3, 4, 5];
for (let i = 0; i < arr2.length; i++) {
  if (arr2[i] === 2) {
    arr2.splice(i, 1); // 删除索引1的元素,数组变为[1,3,4,5]
  }
  console.log(arr2[i]); // 输出: 1, 3, 4, 5(跳过了3之后的元素?实际输出:1,3,4,5)
}

解决方案

  • 使用filter创建新数组(推荐,无副作用):

    const arr = [1, 2, 3, 4, 5];
    const filtered = arr.filter(num => num % 2 !== 0);
    console.log(filtered); // 输出: [1, 3, 5](原数组不变)
    
  • 传统 for 循环从后往前遍历(修改原数组时):

    const arr = [1, 2, 3, 4, 5];
    for (let i = arr.length - 1; i >= 0; i--) {
      if (arr[i] % 2 === 0) {
        arr.splice(i, 1); // 从后往前删除,不影响前面的索引
      }
    }
    console.log(arr); // 输出: [1, 3, 5]
    

结论

JavaScript 提供了丰富的遍历方法,每种方法都有其独特的适用场景和优缺点。掌握这些方法的核心差异和最佳实践,能帮助开发者编写更高效、更安全、更易维护的代码。

核心总结

  • 对象遍历:优先使用Object.keys()/values()/entries()(安全、高效),避免for...in;需遍历所有自有属性(包括不可枚举、Symbol)时使用Reflect.ownKeys()

  • 数组遍历

    • 需控制索引或中断循环:传统 for 循环;
    • 仅遍历元素:for...of
    • 转换元素:map()
    • 筛选元素:filter()
    • 累积计算:reduce()
    • 函数式编程:优先组合使用mapfilterreduce
  • ESLint 规范:遵循no-for-inprefer-map等规则,避免常见错误。

  • 最佳实践:使用纯函数、避免副作用、处理边界情况(如空数组、Symbol 键)。

在实际开发中,应根据具体需求(如是否需要索引、是否修改数组、是否返回结果)选择合适的遍历方法,避免盲目追求 “流行” 方法。同时,结合 ESLint 等工具确保代码规范,提高代码质量和团队协作效率。