数组遍历方法对比深度解析
一句话概括
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-of 靠 break,some/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-in | for-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 ← 回调根本没执行,直接返回唯一元素
空数组 + 无初始值 = 运行时报错。只有一个元素时回调不会触发。保险起见,始终传初始值。
一句话总结
拿到数组,先问自己三句话:我要返回值吗?需要中途刹车吗?会改原数据吗?——三秒答完,方法自然就出来了。