文章

数组遍历方法对比深度解析

数组遍历方法对比深度解析

一句话概括:forEach、map、filter、reduce、some、every 是 JavaScript 中最常用的数组遍历方法,它们在返回值、副作用和使用场景上各有差异,面试中常被要求对比分析。

背景

在日常前端开发中,数组遍历是最常见的操作之一。JavaScript 提供了丰富的数组遍历方法,它们看似相似,但实际上在返回值、是否改变原数组、性能特点等方面有显著差异。掌握这些方法的区别,不仅是编写高质量代码的基础,也是前端面试中的高频考点。

本文将系统对比 forEach、map、filter、reduce、some、every 这六个核心方法,通过代码示例和底层原理分析,帮助你彻底掌握它们的适用场景。

概念与定义

1. forEach 方法

1
array.forEach(callback(currentValue, index, array), thisArg)
  • 返回值:始终返回 undefined
  • 是否改变原数组:取决于回调函数是否修改
  • 特点:最基础的遍历方法,无法 break 或 return

2. map 方法

1
array.map(callback(currentValue, index, array), thisArg)
  • 返回值:返回由回调函数返回值组成的新数组
  • 是否改变原数组:回调函数内部不影响原数组
  • 特点:用于转换数据,一一映射

3. filter 方法

1
array.filter(callback(element, index, array), thisArg)
  • 返回值:返回满足条件的元素组成的新数组
  • 是否改变原数组:不影响原数组
  • 特点:用于筛选数据

4. reduce 方法

1
array.reduce(callback(accumulator, currentValue, index, array), initialValue)
  • 返回值:返回累计计算的结果
  • 是否改变原数组:不影响原数组
  • 特点:最强大,可实现任意累计逻辑

5. some 方法

1
array.some(callback(element, index, array), thisArg)
  • 返回值:返回布尔值(是否有元素满足条件)
  • 是否改变原数组:不影响原数组
  • 特点:短路求值,找到第一个满足条件的元素就停止

6. every 方法

1
array.every(callback(element, index, array), thisArg)
  • 返回值:返回布尔值(是否所有元素都满足条件)
  • 是否改变原数组:不影响原数组
  • 特点:短路求值,遇到不满足条件的元素就停止

最小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const arr = [1, 2, 3, 4, 5];

// forEach: 返回 undefined
const forEachResult = arr.forEach(item => console.log(item));
console.log(forEachResult); // undefined

// map: 返回新数组 [2, 4, 6, 8, 10]
const mapResult = arr.map(item => item * 2);

// filter: 返回新数组 [2, 4]
const filterResult = arr.filter(item => item % 2 === 0);

// reduce: 返回累加结果 15
const reduceResult = arr.reduce((acc, cur) => acc + cur, 0);

// some: 返回 true(存在大于3的元素)
const someResult = arr.some(item => item > 3);

// every: 返回 false(不是所有元素都大于3)
const everyResult = arr.every(item => item > 3);

核心知识点拆解

返回值对比

方法返回值用途
forEachundefined仅遍历,无返回值需求
map新数组数据转换
filter新数组数据筛选
reduce任意值累计计算
someboolean判断是否存在
everyboolean判断是否全部满足

能否中断遍历

  • forEach:无法 break 或 return false 终止
  • some:找到第一个 true 后立即停止
  • every:找到第一个 false 后立即停止
  • map/filter/reduce:始终遍历完整个数组

性能对比

在大多数情况下,这几个方法的性能差异不大。但在极端大数据量场景下:

  • forEach 略快于 map(因为不需要创建新数组)
  • some/every 可能更快(因为短路特性)

常见误区

  1. map 不等于 forEach:map 会创建新数组,forEach 返回 undefined
  2. 不要在 map 中做副作用:应该用 forEach
  3. reduce 可以替代其他方法:所有遍历方法都可以用 reduce 实现

实战案例

案例一:数据转换

1
2
3
4
5
6
7
// 将用户数组转换为 ID 数组
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

const ids = users.map(user => user.id); // [1, 2]

案例二:数据筛选

1
2
3
4
5
6
7
8
9
// 过滤出成年人
const people = [
  { name: 'Alice', age: 17 },
  { name: 'Bob', age: 25 },
  { name: 'Charlie', age: 16 }
];

const adults = people.filter(person => person.age >= 18);
// [{ name: 'Bob', age: 25 }]

案例三:累计计算

1
2
3
4
5
6
7
// 计算购物车总价
const cart = [
  { name: 'iPhone', price: 7999 },
  { name: 'AirPods', price: 1299 }
];

const total = cart.reduce((sum, item) => sum + item.price, 0); // 9298

案例四:条件判断

1
2
3
4
5
6
// 检查是否有权限
const permissions = ['read', 'write', 'delete'];
const required = 'admin';

const hasPermission = permissions.some(p => p === required); // false
const allValid = permissions.every(p => typeof p === 'string'); // true

案例五:reduce 实现 map 和 filter

1
2
3
4
5
6
7
// 用 reduce 实现 map
const mapViaReduce = (arr, fn) => 
  arr.reduce((acc, cur, i) => [...acc, fn(cur, i)], []);

// 用 reduce 实现 filter
const filterViaReduce = (arr, fn) => 
  arr.reduce((acc, cur, i) => fn(cur, i) ? [...acc, cur] : acc, []);

底层原理

forEach 底层实现

1
2
3
4
5
6
7
8
9
10
11
12
Array.prototype.forEach = function(callback, thisArg) {
  // 1. this 转为对象
  const O = Object(this);
  // 2. 获取长度
  const len = O.length >>> 0;
  // 3. 遍历调用
  for (let i = 0; i < len; i++) {
    if (i in O) {
      callback.call(thisArg, O[i], i, O);
    }
  }
};

关键点:

  • i in O 检查索引是否存在(跳过稀疏数组的空位)
  • 使用 call 确保回调函数中的 this 正确
  • 无返回值,始终返回 undefined

map 底层实现

1
2
3
4
5
6
7
8
9
10
11
12
Array.prototype.map = function(callback, thisArg) {
  const O = Object(this);
  const len = O.length >>> 0;
  const result = new Array(len); // 创建新数组
  
  for (let i = 0; i < len; i++) {
    if (i in O) {
      result[i] = callback.call(thisArg, O[i], i, O);
    }
  }
  return result;
};

与 forEach 的区别:

  • 预先创建了与原数组等长的新数组
  • 将回调返回值存入新数组
  • 返回新数组引用

reduce 底层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Array.prototype.reduce = function(callback, initialValue) {
  const O = Object(this);
  const len = O.length >>> 0;
  
  let accumulator, startIndex;
  
  // 确定初始值
  if (arguments.length > 1) {
    accumulator = initialValue;
    startIndex = 0;
  } else {
    // 找到第一个非空索引作为初始值
    let k = 0;
    while (k < len && !(k in O)) k++;
    if (k === len) throw new TypeError('Reduce of empty array');
    accumulator = O[k++];
  }
  
  for (let i = k; i < len; i++) {
    if (i in O) {
      accumulator = callback(accumulator, O[i], i, O);
    }
  }
  
  return accumulator;
};

高频面试题解析

Q1: map 和 forEach 的区别?

答案

  1. 返回值:map 返回新数组,forEach 返回 undefined
  2. 用途:map 用于数据转换,forEach 用于仅遍历
  3. 性能:forEach 略快(无需创建新数组)

Q2: 如何在遍历中中断?

答案

  • 使用 some 或 every 短路特性
  • 使用 for…of 配合 break
  • 使用 for 循环
  • forEach 和 map 无法中断

Q3: reduce 的初始值什么时候需要?

答案

  1. 数组为空时:必须提供初始值,否则报错
  2. 累计结果类型与元素类型不同时:如求最大值、对象累加
  3. 逻辑需要”空状态”时:如累加对象

Q4: 能否用 filter 实现去重?

答案:可以,但需要结合 indexOf 或 Map:

1
2
3
4
5
// 简单去重
const unique = arr.filter((item, index) => arr.indexOf(item) === index);

// Map 去重(推荐)
const unique = [...new Map(arr.map(item => [item, item])).values()];

总结与扩展

方法选择指南

场景推荐方法
仅遍历,无返回值forEach
数据转换(一对一)map
数据筛选filter
累计计算(求和、分类)reduce
判断是否存在some
判断是否全部满足every

扩展阅读

  • find / findIndex:返回第一个满足条件的元素/索引
  • flatMap:先 map 再 flat,扁平化映射
  • sort:排序(会改变原数组)
  • entries / keys / values:遍历键值对/键/值

性能优化建议

  1. 优先使用原生方法(V8 做了大量优化)
  2. 大数据量场景考虑使用 for 循环
  3. 避免在遍历中修改数组
  4. 稀疏数组使用 for…in 或 Object.keys

参考资料:MDN Array.prototype、ECMAScript 规范、V8 引擎源码

本文由作者按照 CC BY 4.0 进行授权