文章

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

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

一句话概括

JS 数组遍历方法分两派:for/for-of 派(可中断、偏过程式)和 map/filter/reduce 派(不可中断、偏声明式)。面试不考你会不会用,而考你能不能从「返回值 + 中断行为 + 副作用」三个维度一秒选对方法。

核心知识点

1. forEach 和 map:一个要过程,一个要结果

1
2
3
4
5
6
7
8
9
10
const nums = [1, 2, 3];

// forEach → 返回值永远是 undefined,只为副作用而生
const a = nums.forEach(n => console.log(n));
console.log(a); // undefined

// map → 返回新数组,原数组纹丝不动
const doubled = nums.map(n => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(nums);    // [1, 2, 3] ← 没变

面试必杀题:[1,2,3].forEach(n => n * 2) 返回什么?—— undefined,不是数组。

2. 谁能半路刹车?中断行为全景

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
27
28
29
30
31
const arr = [1, 2, 3, 4, 5];

// ✅ for-of:break 直接走人
for (const n of arr) {
  if (n === 3) break;
  console.log(n); // 1, 2
}

// ❌ forEach:return 只是跳过当前回调,不是中断!
arr.forEach(n => {
  if (n === 3) return;
  console.log(n); // 1, 2, 4, 5 — 压根没停!
});

// ✅ some:return true 即中断
arr.some(n => {
  if (n === 3) return true;
  console.log(n); // 1, 2
});

// ✅ every:return false 即中断
arr.every(n => {
  if (n === 3) return false;
  console.log(n); // 1, 2
});

// ✅ find/findIndex:找到就停,天然短路
arr.find(n => {
  console.log('检查:', n); // 1, 2, 3(到3就停了)
  return n === 3;
});

速记: for/for-ofbreaksome/every 靠返回值,find/findIndex 自动停。forEach/map/filter/reduce 一律不行。

3. 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
27
28
29
30
const nums = [1, 2, 3, 4, 5];

// 基础:求和(最朴素的用法)
nums.reduce((acc, n) => acc + n, 0); // 15

// 进阶:统计词频(reduce 的独门绝技,其他方法做不到)
const words = ['a', 'b', 'a', 'c', 'b', 'a'];
const freq = words.reduce((acc, w) => {
  acc[w] = (acc[w] ?? 0) + 1;
  return acc;
}, {});
// { a: 3, b: 2, c: 1 }

// 进阶:按属性分组
const users = [
  { name: '张三', role: 'admin' },
  { name: '李四', role: 'user' },
  { name: '王五', role: 'admin' },
];
const byRole = users.reduce((acc, u) => {
  (acc[u.role] ??= []).push(u);
  return acc;
}, {});
// { admin: [张三, 王五], user: [李四] }

// 进阶:管道组合(函数式编程经典模式)
const pipe = (...fns) => init => fns.reduce((v, fn) => fn(v), init);
const addTax = p => p * 1.13;
const fmt = p => ${p.toFixed(2)}`;
console.log(pipe(addTax, fmt)(100)); // "¥113.00"

reduce 的精髓:第二个参数是初始值——不传就取数组第一个元素,空数组不传初始值直接报错。

4. for-of vs for-in:一字之差,天壤之别

1
2
3
4
5
6
7
8
9
10
11
const arr = ['a', 'b', 'c'];

// for-in:遍历键名(注意是字符串!)
for (const key in arr) {
  console.log(typeof key, key); // string '0', string '1', string '2'
}

// for-of:遍历值
for (const val of arr) {
  console.log(val); // 'a', 'b', 'c'
}
对比for-infor-of
遍历内容可枚举属性名(字符串)可迭代对象的值
原型链❌ 会爬原型✅ 不爬
适用场景普通对象数组 / Map / Set / 字符串
性能最慢快(略慢于 for

数组永远不要用 for-in——索引是字符串、还会把原型上的属性也遍历出来。

5. 短路三兄弟:some、every、find

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

// some:任一满足 → true(满足即停)
const hasOdd = data.some(n => {
  console.log('some:', n); // 2, 4, 5(发现奇数,停了)
  return n % 2 !== 0;
}); // true

// every:任一不满足 → false(不满足即停)
const allEven = data.every(n => {
  console.log('every:', n); // 2, 4, 5(不满足偶数,停了)
  return n % 2 === 0;
}); // false

// find:找到即返回元素,找不到返回 undefined
const firstBig = data.find(n => n >= 5); // 5
data.find(n => n > 100);                 // undefined

这三兄弟的共同点:都是短路求值,一旦条件命中就立即停止,不会遍历剩下的元素。非常适合”找第一个满足条件的”或”快速校验”场景。

其实你每天都在用

场景一:列表渲染前的数据转换

后端给了一坨用户对象,前端只需要 ID 列表:

1
const userIds = users.map(u => u.id);

场景二:搜索框实时筛选

输入框每打一个字就筛一次,filter 一行搞定:

1
const results = products.filter(p => p.name.includes(keyword));

场景三:购物车总价

单价 × 数量再求和,reduce 最自然:

1
const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);

场景四:表单提交前校验

所有必填项都不能空,every 天然适合:

1
const isValid = requiredFields.every(f => f.value.trim() !== '');

场景五:从列表里捞特定数据

找某个用户,找不到就给兜底:

1
const user = userList.find(u => u.id === targetId) ?? { name: '未知用户' };

常见误解

❌ 误区一:forEach 里 return 能中断循环

这是面试翻车率最高的陷阱。forEach 的回调是一个函数,里面的 return 只是结束当前这一次回调,循环继续跑。

1
2
3
4
5
// ❌ 你以为停了,其实没有
[1, 2, 3, 4, 5].forEach(n => {
  if (n === 3) return;
  console.log(n); // 1, 2, 4, 5 ← 3 之后还在执行!
});

正确做法:需要中断就用 for-of,或者用 some/find 天然短路。

❌ 误区二:map 和 forEach 可以随便互换

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 用 map 做副作用 —— 产生无用数组,浪费内存,语义错误
users.map(u => sendEmail(u.email));

// ✅ 副作用用 forEach
users.forEach(u => sendEmail(u.email));

// ❌ 用 forEach + push 做转换 —— 啰嗦
const names = [];
users.forEach(u => names.push(u.name));

// ✅ 转换用 map —— 干净
const names = users.map(u => u.name);

一句话原则:要结果用 map,要过程用 forEach。

❌ 误区三:for-in 遍历数组也没啥问题

1
2
3
4
5
6
Array.prototype.custom = '污染';
const arr = ['a', 'b'];

for (const key in arr) {
  console.log(key); // '0', '1', 'custom' ← 原型上的属性也出来了!
}

for-in 会爬原型链,索引是字符串类型,速度最慢。数组遍历请用 for-of 或高阶方法。

❌ 误区四:reduce 不传初始值也无所谓

1
2
3
[].reduce((a, b) => a + b); // ❌ TypeError!

[1].reduce((a, b) => a + b); // 1 ← 回调根本没执行,直接返回唯一元素

空数组 + 无初始值 = 运行时报错。只有一个元素时回调不会触发。保险起见,始终传初始值。

一句话总结

拿到数组,先问自己三句话:我要返回值吗?需要中途刹车吗?会改原数据吗?——三秒答完,方法自然就出来了。

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