JavaScript 遍历方法详解
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. 函数式编程场景
- 优先使用
map、filter、reduce等高阶函数,代码更简洁、声明式,可维护性更高。 - 鼓励使用纯函数(无副作用),避免在回调函数中修改外部变量或原数组。
- 复杂逻辑可组合使用高阶函数(如
map().filter().reduce()),替代多层 for 循环。
ESLint 规范与最佳实践
ESLint 对遍历方法的建议
ESLint 作为前端常用的代码检查工具,对遍历方法有明确的规范建议,旨在提高代码质量和一致性:
-
禁止使用 for...in 遍历数组(规则:
no-for-in)- 原因:for...in 会遍历数组的非数字属性(如自定义属性、原型链属性),导致意外结果。
- 解决方案:使用
for...of、forEach、传统 for 循环或Object.keys()替代。
-
优先使用安全的对象遍历方法(规则:
prefer-object-spread、prefer-destructuring)-
原因:
for...in可能遍历继承属性,存在安全风险。 -
解决方案:使用
Object.keys()/values()/entries()结合解构赋值,如:// 推荐 for (const [key, value] of Object.entries(obj)) { // 处理逻辑 } // 不推荐 for (const key in obj) { if (obj.hasOwnProperty(key)) { // 处理逻辑 } }
-
-
禁止在循环中修改数组(规则:
no-unsafe-optional-chaining、no-param-reassign)- 原因:在
forEach、for...of等循环中修改数组长度或元素,可能导致遍历不完整或重复遍历。 - 解决方案:使用
map、filter等高阶函数创建新数组,而非修改原数组。
- 原因:在
-
优先使用函数式方法替代 forEach(规则:
prefer-map、prefer-filter、prefer-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);
-
函数式编程最佳实践
-
使用纯函数
-
回调函数不应修改外部变量或原数组,仅依赖输入参数返回结果。
-
示例:
// 纯函数(推荐) const double = num => num * 2; const doubled = [1, 2, 3].map(double); // 非纯函数(不推荐) let total = 0; [1, 2, 3].forEach(num => { total += num; // 修改外部变量 });
-
-
避免回调地狱
-
多层
forEach嵌套会降低代码可读性,可使用map、filter、reduce组合替代。 -
示例:
// 不推荐(嵌套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)]; }, []);
-
-
合理使用解构赋值
-
遍历对象键值对时,使用解构赋值简化代码。
-
示例:
Object.entries(obj).forEach(([key, value]) => { console.log(`${key}: ${value}`); });
-
-
处理边界情况
-
数组为空时,
reduce必须提供初始值,避免抛出错误。 -
示例:
// 推荐(提供初始值) const sum = [].reduce((acc, num) => acc + num, 0); // 0 // 不推荐(无初始值,空数组会报错) const sum = [].reduce((acc, num) => acc + num); // TypeError
-
-
性能优化
- 大型数组(百万级元素)遍历优先使用传统 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...in、Object.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)。
- map/filter 回调:
-
确保 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. 遍历过程中修改数组的问题
问题:在forEach、for...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(); - 函数式编程:优先组合使用
map、filter、reduce。
-
ESLint 规范:遵循
no-for-in、prefer-map等规则,避免常见错误。 -
最佳实践:使用纯函数、避免副作用、处理边界情况(如空数组、Symbol 键)。
在实际开发中,应根据具体需求(如是否需要索引、是否修改数组、是否返回结果)选择合适的遍历方法,避免盲目追求 “流行” 方法。同时,结合 ESLint 等工具确保代码规范,提高代码质量和团队协作效率。