做前端这几年,我面试过不少人,也看过不少新人写的代码。有个现象特别有意思:很多写了两三年 JavaScript 的人,遇到数组遍历要么只会用 for 循环死磕,要么无脑用 map 或者 forEach,压根没搞清楚每个方法返回什么、什么时候该用哪个。今天就把 JavaScript 数组遍历里最常用的七个方法讲透:forEach、map、filter、reduce、every、some、find。
这些方法看起来都只是“循环遍历数组”,但背后的设计思路完全不同:有的用来执行副作用,有的用来做数据映射,有的用来做筛选,有的用来做聚合,还有的能提前终止遍历。这篇文章会用实际场景拆解它们的差异、底层机制和藏在细节里的坑,无论你是刚入门的新手,还是写了几年业务代码但没系统梳理过的老手,都能有所收获。
1. 遍历之前:先搞清楚你要什么
数组遍历的本质,就是对数组里的每个元素执行一次回调函数。回调函数统一接收三个参数:当前元素item、当前索引index、数组本身array。这个签名在 forEach、map、filter、reduce 里基本一致,只有 reduce 例外,它额外多一个“累计值”参数。
但“遍历”两个字掩盖了方法之间的巨大差异。同样是走一遍数组,编译器的意图可以是:
- 只是想对每个元素做点事(比如打印、赋值);
- 想根据每个元素生成一个新元素,最终得到等长的新数组;
- 想把不满足条件的元素剔除,保留子集;
- 想找到第一个符合条件的元素;
- 想确认是否存在至少一个/所有元素满足条件;
- 想把整个数组合并成一个值。
如果你没想清楚自己要什么就去选方法,代码大概率会写得别扭。比如有人用 map 做遍历打印,有人用 forEach 收集结果还要额外定义外部变量,还有人为了找一个元素把整个数组 filter 一遍再取[0]。这些都是没有按照“返回值”来选方法导致的。
先用一张速览表把这些方法的关键差异列出来,后面再逐个拆。
| 方法 | 返回值 | 是否生成新数组 | 能否提前停止 | 典型用途 |
|---|---|---|---|---|
| forEach | undefined | 否 | 不能 | 执行副作用(打印、写入) |
| map | 新数组,长度与原数组相同 | 是 | 不能 | 数据映射、转换 |
| filter | 新数组,长度为满足条件的元素个数 | 是 | 不能(会遍历全部) | 筛选子集 |
| find | 第一个满足条件的元素值,没有则 undefined | 否 | 找到后可以停止 | 查找单个元素 |
| some | 布尔值 | 否 | 遇到 true 停止 | 是否存在满足条件 |
| every | 布尔值 | 否 | 遇到 false 停止 | 是否全部满足 |
| reduce | 累计后的任意类型值 | 否 | 不能(除非手动 throw) | 求和、分组、扁平化 |
这张表你得牢记一点:方法名带“返回什么”,就决定了它适合什么场景。比如 map 核心是“映射”,不是“遍历”;filter 核心是“筛选”,筛选必然产生新数组;some/every 是“判断”,结果必须是布尔值。想通这一点,你选方法时就不会犹豫。
2. 逐个拆解:七个方法的核心用法与原理
2.1 forEach:只做事,不返回
forEach 的语法长这样:
arr.forEach(callback(currentValue, index, array) { // 你的操作 }, thisArg);它非常简单,拿到数组每个元素依次执行回调,没有返回值,返回值是 undefined。这意味着你用const res = arr.forEach(...)拿到的永远是 undefined。
forEach 适合什么场景?“副作用”场景,也就是你想对数组里的每个元素执行一些操作,但操作的结果不需要再组合成一个新数组。比如:
const users = [{ name: '张三' }, { name: '李四' }]; users.forEach((user, index) => { console.log(`第 ${index + 1} 个用户是:${user.name}`); }); // 输出: // 第 1 个用户是:张三 // 第 2 个用户是:李四还有常见的场景是把表单数据逐条提交、记录埋点、给每个 DOM 节点绑定事件。这些操作的特点是“做完就完了”,不需要把结果收集回来。
很多人刚学写代码时喜欢这样:
const result = []; arr.forEach(item => { if (item.age > 18) { result.push(item); } });这种写法能用,但属于“用 forEach 的副作用去模拟 filter + map 的组合”。本质问题是:你用外部数组result作为容器,违背了函数式编程中“不变性”的原则。更好的做法是直接arr.filter(item => item.age > 18),后面会讲。
一个容易忽略的点:forEach 不会修改原数组,但如果数组里的元素是对象,回调中修改对象的属性是会生效的。因为对象是引用传递,你改的是对象本身。比如:
const arr = [{ count: 1 }]; arr.forEach(item => { item.count += 1; }); console.log(arr[0].count); // 2这不算 bug,但要意识到 forEach 的“不改变原数组”只是说你不能通过回调的返回值去组合一个新数组,或者对原始数组进行结构层面的增删。
2.2 map:映射,不是遍历
map 是“映射”的缩写。它的定义是:对数组每一项执行回调,把回调的返回值收集起来,构成一个等长的新数组。
const newArr = arr.map(callback(currentValue, index, array) { // 必须 return,否则新数组对应位置是 undefined return transformedValue; });举个最常见的例子:取对象数组中某个字段。
const users = [ { name: '张三', age: 20 }, { name: '李四', age: 25 }, ]; const names = users.map(user => user.name); console.log(names); // ['张三', '李四']再比如把数字都翻倍:
const numbers = [1, 2, 3]; const doubled = numbers.map(n => n * 2); console.log(doubled); // [2, 4, 6]map 最大的坑是:回调忘了写 return。新手经常写出这样的代码:
const arr = [1, 2, 3]; const doubled = arr.map(item => { item * 2; // 没有 return }); console.log(doubled); // [undefined, undefined, undefined]因为花括号{}把函数体包起来了,必须显式return。所以要么写单行表达式arr.map(item => item * 2),要么在函数体里加上return。
另一个容易被忽略的点是:map 出来的新数组长度与原数组相同,但元素的值由回调决定。如果你不需要产生新的数组内容,只是想遍历,用 forEach 更合适——用 map 做循环会产生一个没人要的临时数组,浪费内存且语义混乱。
2.3 filter:筛选出符合条件的新数组
filter 的作用是“过滤”。它会对原数组每一项执行回调,回调返回值为true(或真值)时,把这一项保留下来,最终返回一个新数组。
const newArr = arr.filter(callback(currentValue, index, array) { return booleanValue; // 如果为 true,保留当前元素 });经典场景:筛选出年龄大于 18 的用户。
const users = [ { name: '张三', age: 17 }, { name: '李四', age: 21 }, { name: '王五', age: 18 }, ]; const adults = users.filter(user => user.age >= 18); console.log(adults); // [{ name: '李四', age: 21 }, { name: '王五', age: 18 }]filter 不会修改原数组,总是返回一个新数组。如果没有任何元素满足条件,返回的是空数组[],不是 undefined。
这里有个常见的误解:有人以为 filter 会“删除”原数组中的元素。实际上原数组保持不变。如果你要做删除操作,比如从某个数组中移除 id 为 3 的元素,正确姿势是:
const newArr = arr.filter(item => item.id !== 3);然后把newArr作为新状态使用,而不是去 splice 原数组。这在 React、Redux 这类强调不可变数据的环境中几乎是标配操作。
filter 的性能也值得说一句:它会完整遍历数组,所以如果你只想找一个满足条件的元素,用 filter 会做很多无用功,这时应该用find。
2.4 find:只找第一个,找到就停
find 返回第一个满足回调条件的元素值,如果没有,返回undefined。
const foundItem = arr.find(callback(currentValue, index, array) { return booleanValue; });它和 filter 最大的区别是:filter 返回包含所有满足条件的数组,find 只返回第一个命中对象。比如:
const users = [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' }, ]; const target = users.find(user => user.id === 2); console.log(target); // { id: 2, name: '李四' }如果找不到,返回 undefined。对应到实际业务里非常常见:根据 ID 找一条订单、根据路由路径匹配一个配置项、在字典数组里查某个状态对应的文本。
find 还有一个隐藏优势:它会在找到第一个满足条件的元素后停止遍历,不再继续检查剩余元素。对应到长列表场景,如果满足条件的元素靠前,效率会比 filter 高不少。虽然数组遍历本身开销不大,但养成正确的习惯还是很有必要的。
有一个小知识点:与 find 配套的findIndex同样很好用,它返回的是索引而不是元素本身。当你只需要索引时,不要arr.find(...)再indexOf,直接用findIndex。
2.5 some 和 every:满足一次还是满足所有
把 some 和 every 放一起,因为它们是“逻辑判断”的一对。它们的返回值都是布尔值。
// some:只要有一个元素满足条件,就返回 true,否则 false const hasAdult = arr.some(item => item.age >= 18); // every:所有元素都满足条件,才返回 true,否则 false const allAdults = arr.every(item => item.age >= 18);这两个方法在语义上分别等同于逻辑或(||)和逻辑与(&&)在数组上的展开。比如:
const numbers = [1, 2, 3, 4, 5]; // 有没有偶数? console.log(numbers.some(n => n % 2 === 0)); // true // 是否全部都是正数? console.log(numbers.every(n => n > 0)); // true // 是否全部都是偶数? console.log(numbers.every(n => n % 2 === 0)); // false这里有一个值得利用的特性:some 只要回调返回 true 就会立刻停止遍历,every 遇到 false 会立刻停止遍历。这是它们和 forEach/map/filter 不一样的地方,你可以利用这个特性实现提前退出。
举个例子,判断一个数组中是否包含某个值,传统做法用indexOf或者 includes,但如果是“是否包含某个满足复杂条件的对象”,some 是最好的选择:
const users = [ { name: '张三', role: 'admin' }, { name: '李四', role: 'user' }, ]; const hasAdmin = users.some(user => user.role === 'admin'); console.log(hasAdmin); // true同理,如果需要校验“所有输入都不为空”,可以:
const values = ['abc', 'def', '']; const allValid = values.every(value => value.trim() !== ''); console.log(allValid); // falseevery 和 some 同样不会改变原数组。它们适合放在if条件里做校验,而不是用来收集数据。
2.6 reduce:所有方法的底层能力,都在这了
reduce 是这七个方法里最强大、也最容易让人懵圈的一个。它的回调签名和前面几个不同,有四个参数:
arr.reduce(callback(accumulator, currentValue, currentIndex, array) { // accumulator 是上一次回调的返回值 // 返回的结果会作为下一次调用的 accumulator return newAccumulator; }, initialValue);第一次执行时,accumulator的取值分两种情况:
- 如果你提供了
initialValue,第一次的accumulator就是initialValue,第一次的currentValue是数组第一个元素。 - 如果你没提供初始值,第一次的
accumulator是数组第一个元素,currentValue是数组第二个元素。也就是说,reduce 会从第二个元素开始遍历。
最经典的求和:
const numbers = [1, 2, 3, 4]; const sum = numbers.reduce((acc, cur) => acc + cur, 0); console.log(sum); // 10这行代码等价于:
let sum = 0; for (const cur of numbers) { sum += cur; }注意,如果不传初始值,写成:
const sum = [1, 2, 3, 4].reduce((acc, cur) => acc + cur);也能得到 10,因为第一次 acc 是 1,cur 是 2,累加后 acc 变成 3,然后 cur 是 3,以此类推。但有一个致命问题:数组为空时,不传初始值会直接抛错。所以除非你确定数组至少有一个元素,否则最好始终传入初始值。
reduce 不只能做加法,它能做任何“把一个数组归并成单个值”的操作。后面我会专门讲几个进阶案例,比如分组、统计词频、把数组转对象等。
3. 进阶用法与组合实战
3.1 链式调用:一出手就是清晰的数据管道
这几个方法最爽的用法是串联起来,形成一个“数据管道”。每一个方法返回的值能被下一个方法接收,中间不会产生冗余的中间变量。
假设你的业务是:从一堆订单中筛出已付款订单,把它们金额打 9 折,再算出总金额。
const orders = [ { id: 1, paid: true, amount: 100 }, { id: 2, paid: false, amount: 50 }, { id: 3, paid: true, amount: 200 }, ]; const total = orders .filter(order => order.paid) // 1. 筛选已付款 .map(order => order.amount * 0.9) // 2. 计算折扣价 .reduce((sum, price) => sum + price, 0); // 3. 求和 console.log(total); // 270这不是炫技。你想想如果用循环怎么写?得写三层:先建数组存过滤结果,再建数组存打折金额,最后累计。链式调用每一步只做一件事,变量名都不需要额外定义,可读性反而更好。
不过要注意:链别太长。一旦超过四五个方法,别人阅读时就需要在脑子里同时维护每一步的数据状态,反而降低了可读性。如果中间处理逻辑很复杂,建议拆成带语义的变量,或者用命名函数:
const paidOrders = orders.filter(order => order.paid); const discountedPrices = paidOrders.map(order => order.amount * 0.9); const total = discountedPrices.reduce((sum, price) => sum + price, 0);拆开写更容易打断点调试。我自己一般是先写成链式,等改出 bug 再拆开打 log,最终提交前看阅读难度决定是否保留。
3.2 用 reduce 做分组和统计
说实话,reduce 最实用的进阶场景不是求和,而是“把数组变成对象”。比如把一堆用户按照角色分组,你会怎么做?
有人会手写 for 循环,维护一个空对象,然后判断 key 是否存在。用 reduce 一行搞定:
const users = [ { name: '张三', role: 'admin' }, { name: '李四', role: 'user' }, { name: '王五', role: 'admin' }, ]; const groupByRole = users.reduce((result, user) => { // 如果当前角色还没有分组,先初始化一个空数组 if (!result[user.role]) { result[user.role] = []; } result[user.role].push(user); return result; }, {}); console.log(groupByRole); // { // admin: [{ name: '张三', role: 'admin' }, { name: '王五', role: 'admin' }], // user: [{ name: '李四', role: 'user' }] // }关键点在于:reduce 的回调必须返回result。很多初学者在这里忘记 return,导致第二次回调时result变成 undefined,然后if (!result[user.role])直接报错。如果你也踩过这个坑,记住一句话:reduce 回调的返回值会成为下一次回调的第一个参数,不 return 就断链。
同样,统计数组中每个单词出现的次数:
const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']; const frequency = words.reduce((counter, word) => { counter[word] = (counter[word] || 0) + 1; return counter; }, {}); console.log(frequency); // { apple: 3, banana: 2, orange: 1 }顺便一提,像这种“数组转对象”“按 key 分组”的逻辑,在很多业务代码里反复出现。ECMAScript 新规范里也提供了Object.groupBy之类的 API,但浏览器兼容性还不够全面,完全可以用 reduce 自己实现,不依赖任何库。
3.3 在 forEach 里如何“中断”:三种替代思路
很久以前我也困惑过:为什么forEach里的break会直接报语法错误?这个得从设计意图说起。forEach 的设计目标是“对每个元素无差别地执行回调”,它是一个声明式 API,不承诺你可以在中途退出。换句话说,它的底层实现是一个只会走到底的循环,不像for...of那样受break控制。
那如果业务上确实需要在遍历到某个元素后提前结束呢?有三种常见做法。
第一种:用some()替代 forEach 实现“找到即停止”。
如果你是在“找有没有满足条件的元素”,那 some 本身就是为这个场景设计的。回调返回 true 时,some 立刻返回 true,后面不再遍历。
const arr = [1, 2, 3, 4, 5]; const found = arr.some(item => { console.log('当前值:', item); if (item === 3) { return true; // 返回 true 后,some 立刻停止 } return false; }); // 只会打印到 3第二种:用find()替代,找到后直接拿结果。
如果目标是“找到元素本身”,find 天然支持提前停止,前面已经讲过。
第三种:改用for...of循环,用 break 退出。
如果你必须在遍历过程中根据条件跳出,且逻辑并不是简单的“查找”,那就老老实实用最原始的循环,不要硬套数组方法:
const arr = [1, 2, 3, 4, 5]; for (const item of arr) { if (item === 3) break; console.log(item); }有人可能想用“抛异常”来中断 forEach,这确实能让遍历停下来,但异常本身的成本不低,而且会让代码陡增复杂度。除非你写的是一个 do-while 风格的复杂遍历,否则我强烈不建议为“中断”小心翼翼去 try-catch。
3.4 异步遍历的坑:forEach 不等人
这是实战里翻车率最高的一个问题。如果你在forEach里写await,你会发现根本不等你:
async function handleList(items) { items.forEach(async (item) => { await saveToServer(item); // 并不会按顺序等待 }); console.log('全部完成'); // 这行会先执行 }原因很简单:forEach不会理会回调返回的 Promise。它只是把每个回调丢出去执行了,然后在继续遍历下一个元素。await 只作用在回调函数内部,没有作用到 forEach 层面。所以如果你需要串行地一个个等异步操作完成,有两个方案。
方案一:用for...of,这是最简单的串行写法。
async function handleList(items) { for (const item of items) { await saveToServer(item); // 每循环一次,等下一次完成 } console.log('全部完成'); }方案二:如果异步操作之间互不依赖,希望并发执行,用Promise.all加上 map:
async function handleList(items) { await Promise.all(items.map(item => saveToServer(item))); console.log('全部完成'); }这里用map而不是forEach,是因为 map 会收集每个回调返回的 Promise。如果你用 forEach,回调的返回值被丢弃,Promise.all就无法拿到这些 Promise。这个差别是“返回值”设计带来的关键影响,也是我反复强调“方法返回值决定了它的用途”的原因。
4. 常见问题与排查实录
4.1 记不住回调参数顺序怎么办
这是新手最常问的问题。所有遍历方法的回调都是(item, index, array)的顺序,也就是:
- 第一个参数是元素本身;
- 第二个参数是下标;
- 第三个参数是正在遍历的原数组。
只有 reduce 例外,它的回调是(accumulator, item, index, array),也就是累计值要放在第一个参数。
你可以通过一个小技巧记忆:先判断“这个东西是不是每次遍历都有的东西”,如果是元素,就排在前面。reduce 多了累计值,累计值又是最“特殊”的,所以它排在第一位。
4.2 map 和 forEach 到底选哪个
这是面试必问题。一句话版答案:需要生成一个与输入数组一一对应的新数组时用 map,不需要返回值只是要执行操作时用 forEach。
细节版:map 的返回值是一个新数组,而且长度和原数组完全相同。如果你用 map 却不要它的返回值,等于浪费了一部分内存还降低了代码可读性。比如:
// 不推荐:为了遍历而用 map,产生的数组丢了 [1, 2, 3].map(item => { console.log(item); }); // 推荐:forEach 就是为这种场景设计的 [1, 2, 3].forEach(item => { console.log(item); });反之,如果你要得到一个新数组,也不要用 forEach 加外部数组。比如:
// 不推荐:手动 push 掩盖了“映射”语义 const result = []; [1, 2, 3].forEach(item => { result.push(item * 2); }); // 推荐:map 本身就是做映射 const result = [1, 2, 3].map(item => item * 2);4.3 filter 和 find 使用场景混淆
一句话版:要所有满足条件的元素,用 filter;只要第一条满足条件的记录,用 find。
我见过不少代码这样写:
const user = users.filter(user => user.id === userId)[0];如果确定 id 唯一,这里更合适的应该是users.find(user => user.id === userId)。filter 会遍历完整个数组,把所有相同 id 的都筛出来(虽然只有一条),再通过下标取第一个。find 会在找到第一条后立刻返回,语义也更直接。
当然,如果你的目的是“得到所有匹配项”,那就该用 filter,这没悬念。这里还有一个小的知识点:filter 即使没有任何匹配项,返回的也是空数组,所以可以用filter(...).length > 0来判断是否存在。但既然有some和find,直接用它们做判断和取元素会更好,也不用担心返回 undefined 的问题。
4.4 遍历过程中修改原数组引发的诡异问题
很多人不知道 JS 数组遍历方法不推荐在回调中直接对数组进行 push、pop、splice 等操作。比如:
const arr = [1, 2, 3, 4, 5]; arr.forEach(item => { if (item === 2) { arr.push(999); } console.log(item); }); // 打印 1 2 3 4 5这里的 999 并不会被遍历到,因为 forEach 在遍历前会把数组长度缓存下来。也就是说,你在过程中新增的元素不会出现在本次遍历中。但反过来,如果你用splice删除了元素,又会导致后续索引错乱,甚至跳过元素:
const arr = [1, 2, 3, 4, 5]; arr.forEach((item, index) => { if (item === 3) { arr.splice(index, 1); // 删除当前元素 } }); console.log(arr); // [1, 2, 4, 5]看起来变化不大,但如果下一个元素的索引是 3,原来索引为 5 的元素会移动到索引 4,而 forEach 不会重新读取数组,它继续访问索引 4,就会拿到移动过来的元素。这种索引混乱特别不容易察觉,尤其是遍历过程中连续删除多个元素时。
我的建议是:不要在遍历时修改数组本身。如果你要删除一批元素,用 filter 生成一个新数组再整体替换;如果你要在遍历过程中对数组做增删,改成 while 循环并手动维护索引,逻辑会清楚很多。
4.5 稀疏数组:map 会保留空洞,forEach 会跳过
JavaScript 的数组允许有“洞”,比如:
const arr = [1, , 3]; // 中间有一项是空位注意,这里的空位是undefined但又不完全是undefined值,它是数组上的“缺口”。
不同方法的处理方式不同:
forEach、filter、some、every会跳过空洞;map会保留空洞,返回的数组中对应位置仍然是空洞;find会把空洞当成undefined进行处理。
我们验证一下:
const arr = [1, , 3]; arr.forEach((item) => { console.log(item); // 输出 1 和 3,第二个元素被跳过 }); const mapped = arr.map(item => item * 2); console.log(mapped); // [2, 空洞, 6],空洞还在 const filtered = arr.filter(item => true); console.log(filtered); // [1, 3],空洞被过滤掉了如果数据源是后端返回的 JSON 数组,通常不会有空洞,但如果你自己创建数组时用了new Array(3),里面就是三个空洞。所以处理这种数组时,如果需要保证不出现未知的 undefined,可以用Array.from或者展开运算符把空洞转成 undefined:
const arr = Array.from(new Array(3)); // [undefined, undefined, undefined]4.6 reduce 忘了传初始值,空数组直接报错
刚才提过一句,这里展开说:
const arr = []; const sum = arr.reduce((acc, cur) => acc + cur); // TypeError: Reduce of empty array with no initial value因为没传初始值时,reduce 会把第一项当成初始值。数组为空,它连“第一项”都没有,只能抛错。所以凡是可能为空数组的统计,必须带上初始值:
const sum = arr.reduce((acc, cur) => acc + cur, 0);还有一个细节:即使数组非空,如果回调里返回了undefined,下一次的acc就变成 undefined,后续加减乘除很容易变成 NaN。所以 reduce 的回调一定要设置明确的返回值。
另外,如果你用 reduce 统计属性数量和,初始值类型决定了返回类型。比如统计数组中有多少个有效值,如果初始值是 0,结果一定是数字;如果你想返回一个对象,初始值就必须是{}。
5. 选型速查与我的实操总结
5.1 一张表助你直接抄作业
| 需求 | 使用的方法 | 示例 |
|---|---|---|
| 给每个元素执行操作(打印、埋点) | forEach | arr.forEach(item => { sendLog(item) }) |
| 数组元素逐个转换 | map | arr.map(item => item.price * 2) |
| 筛掉不满足条件的元素 | filter | arr.filter(item => item.isValid) |
| 只需要第一个满足条件的元素 | find | arr.find(item => item.id === 5) |
| 判断是否存在匹配项 | some | arr.some(item => item.score > 60) |
| 判断所有项都符合条件 | every | arr.every(item => item.score > 60) |
| 把数组聚合为单个值 | reduce | arr.reduce((sum, item) => sum + item.count, 0) |
如果你拿不准,就按顺序问自己:我要不要新数组?要长度相等的用 map,要子集用 filter;要不要布尔值?用 some/every;要不要单个元素?用 find;要不要执行副作用?用 forEach;要不要合并成一个值?用 reduce。
5.2 我在项目里的实际体会
最后分享几个我个人的经验,不算标准答案,但实战中真的有用。
第一,不要迷信函数式写法。数组方法确实能写出很优雅的代码,但如果不分场景全用链式,debug 时你会很痛苦。我见过最夸张的一段代码,连续调用六个方法,最后返回结果不对,只能一行行拆开加 console.log。这种时候我就会想:与其写出零注释的“高密度管道”,不如用一两个中间变量把每步结果存下来。
第二,在 React 生态里,尽量用 map 不可变更新,少用 forEach 改外部变量。React 和 Redux 的核心理念是“状态不可变”,你放一个外部数组在 forEach 里 push 再 setState,很容易产生被共享的引用,导致状态偷偷被修改。用map、filter生成新数组再赋值,才是符合框架心智模型的做法。
第三,性能差异在绝大多数业务场景里可以忽略。有些工程师为了一个几千长度的数组,把 filter 改成手动 for 循环,说“更快”。实际上,V8 引擎对这几个原生方法都有非常深的优化,多数场景下你这点性能差距远小于网络请求的消耗。真正影响性能的反而是滥用链式,产生多个临时数组。如果数组极大(十万、百万级别),或者需要按索引随机访问和提前中断,我会直接回到 for 或 while。但业务开发中,for 循环应该只作为“最后手段”出现。
第四,写代码前先想清楚方法的返回值。这个我反复强调,因为它真的是根因。只要你想清楚“forEach 返回 undefined、map 返回新数组、filter 返回子集、find 返回元素、some/every 返回布尔、reduce 返回累计值”,你在选方法时完全不用查文档,也不用试错。所谓“数组遍历方法详解”,说到底就是对你“意图”的确认——你想要的到底是过程、结果,还是状态?
这些方法用熟了以后,你会发现 JavaScript 里很多逻辑都可以用“数据管道”的方式写出来。少一些临时变量,少一些副作用,代码会更好维护。希望这篇拆解能让你对数组遍历这件事建立起自己的判断框架,至少下次在代码 review 时,能一眼看出来哪里用了错误的遍历方法。