JavaScript数组遍历七方法详解:从forEach到reduce
发布时间:2026/9/30 4:07:08来源:尧图网络
数组遍历是所有JavaScript开发者都绕不开的日常操作。forEach、map、filter、reduce、every、some、find这七个方法名字看着都熟但很多人真正写起来总是差点意思——要么不知道选哪个要么用着用着踩进坑里。今天我就把它们全部摊开讲一遍从回调签名、返回值、使用场景到常见报错一次讲透。这篇文章既适合刚开始学JS基础的新手照着抄作业也适合写了几年代码但没认真抠过细节的老手查漏补缺。先给大家吃个定心丸这七个方法没有谁替代谁的关系它们解决的是不同类型的问题。搞懂它们的分工比硬背API文档重要一百倍。1. 七个方法的分工先看懂设计思路再谈写法很多人学数组方法喜欢一个个记语法结果今天记明天忘。我建议反过来先看它们到底在解决什么类型的问题再去看写法思路会顺很多。1.1 遍历、变换、筛选、聚合、判定、查找一类方法解决一类问题JavaScript的数组遍历方法看起来很多其实可以按“目的”归成几类只遍历不改数据forEach它就是“逛一圈”看看每一个元素不做任何返回。变换数据map把原数组的每个元素映射成一个新值得到一个长度相等的新数组。筛选数据filter从原数组里挑出符合条件的元素组成新数组。聚合数据reduce把数组“揉”成一个值这个值可以是数字、字符串、对象、数组任意类型都行。条件判定every和some一个要求全部满足一个要求至少一个满足都返回布尔值。查找元素find找出第一个满足条件的元素本身找不到就返回undefined。记住这个分类方法你在写代码的时候脑子里就有了一张地图。比如你想把一个数组里的所有商品价格翻倍这就是“变换”需求直接用map你想找出价格超过100元的第一个商品这就是“查找”需求用find。1.2 统一的回调签名element、index、array 和三参数强制记忆这七个方法虽然用途不同但回调函数的签名非常一致function callback(element, index, array) { // element当前遍历到的元素 // index当前元素的下标 // array正在被遍历的原数组 }这三个参数是固定的但实际开发中90%的情况只用得到第一个element。很多新手会疑惑后两个参数到底什么时候用我举两个真实例子。index最常用的场景是列表渲染时需要带序号array参数用的场景相对少但如果你需要在回调里“回头看”整个数组——比如判断当前元素是不是最后一项就可以用array的length来对比。还有一个容易被忽略的参数叫thisArg它是方法第二个可选参数用来指定回调函数里this的指向。老实说现在已经不怎么推荐用thisArg了因为箭头函数不绑定this用了也没效果。了解有这个东西就行没必要花太多时间研究。2. 逐个拆解forEach、map、filter 的用法与陷阱这三个方法是最常用的“三件套”但也是新手最容易出错的地方。我一个个说清楚。2.1 forEach只负责“逛”不负责“改”也停不下来forEach的语义非常纯粹遍历数组对每个元素执行一次回调不返回任何东西。它不改变原数组除非你在回调里主动修改。const numbers [1, 2, 3, 4]; numbers.forEach((num, index) { console.log(第${index 1}个数字是${num}); });这里要重点提醒一个高频坑想在forEach里用break或return打断循环是无效的。很多从C语言、Java转过来的同学会有这个习惯写如下代码// 错误示范想找到数字3就停止遍历 [1, 2, 3, 4].forEach(num { if (num 3) { break; // 这里会直接报 SyntaxError } });上面代码直接会报SyntaxError: Illegal break statement。就算你用return它也只代表“跳过当前这一次回调”不会终止整个遍历。如果确实需要“找到就停”你应该选some、find、findIndex或者老老实实用for...of循环// 正确做法之一用 for...of for (const num of [1, 2, 3, 4]) { if (num 3) break; }还有一点要注意forEach不会跳过空位但会跳过值为undefined的元素不对我纠正一下——forEach会跳过稀疏数组中的“空槽”但undefined是正常存在的元素它不会跳过。这个细节我放在后面第5章详细讲。2.2 map一对一映射回调没有返回值等于白写map的含义是“映射”它把数组的每个元素映射成另一个值返回一个新数组。新数组的长度永远和原数组一样长。const prices [100, 200, 300]; const withTax prices.map(price price * 1.1); // 返回 [110, 220, 330]最容易犯的错是在回调里做了操作却忘记返回结果。// 错误示范想给每个元素加1结果得到 [undefined, undefined, undefined] const arr [1, 2, 3]; const result arr.map(num { num 1; // 忘记写 return }); console.log(result); // [undefined, undefined, undefined]map的回调必须有返回值否则数组里的每个位置都会被填充成undefined。如果你发现map的结果里有大量undefined第一反应应该是检查回调体里的return是不是漏了而不是去怀疑别的地方。另外要注意map返回的新数组不会改动原数组。如果你看到某个方法调用后原数组值变了说明方法内部直接操作了原对象这不关map的事。2.3 filter只留符合条件的项回调本质是一个判官filter接收一个“判定函数”函数返回真值就保留当前元素返回假值就过滤掉最终得到一个新数组。const products [ { name: 西瓜, price: 30 }, { name: 鼠标, price: 99 }, { name: 键盘, price: 300 }, ]; const cheap products.filter(item item.price 100); // 得到 [{ name: 西瓜, price: 30 }, { name: 鼠标, price: 99 }]这里有个很关键的细节filter的回调返回的是“真值”或“假值”。也就是说返回1、a、{}都会被视作“通过”返回0、NaN、、null、undefined、false会被视作“不通过”。这和 true完全不是一回事。举个例子如果你想筛出所有非空字符串const list [a, , b, null, c, undefined]; const valid list.filter(item item); // 注意这里返回的是所有“真值”结果是 [a, b, c]这个写法很简洁但也容易埋坑如果业务上希望保留数字0这种写法就会把0过滤掉。所以建议回调里写成明确的布尔表达式比如item ! null item ! 。filter还有一个特点它和原数组元素顺序保持一致这一点在看日志和调试的时候很舒服。3. reduce、every、some、find更高级的四种用法这四个方法的“抽象程度”比前面三个高一些尤其是reduce很多人第一次接触会觉得难懂。其实一旦理解它的模型你会反过来说“真香”。3.1 reduce从累加器到万能工具箱reduce直译是“缩减”它可以把数组中的多个值归并成一个值。它接收两个参数回调函数和初始值。const numbers [1, 2, 3, 4]; const sum numbers.reduce((acc, num) acc num, 0); // 计算过程0 1 - 1, 1 2 - 3, 3 3 - 6, 6 4 - 10 // 结果是 10理解reduce的关键是理解那个“累加器”acc。第一轮执行时acc是初始值0之后每一轮的acc都是上一轮回调的返回值。一句话总结你得告诉它“下一次用到什么东西”它才能持续累加。这里有一个很常见的疑问初始值能不能省略可以但省略时第一轮的acc会自动取数组的第一个元素num取第二个元素。我个人建议除非你非常确定自己在干什么否则一律加上初始值。省略初始值有两个风险如果数组是空的调用reduce会直接报TypeError: Reduce of empty array with no initial value。逻辑不直观阅读代码的人容易误解首轮处理是什么。reduce不止能求和它还能做很多看起来“不该它做”的事。比如把数组转成对象const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 }, ]; const userMap users.reduce((acc, user) { acc[user.id] user; return acc; }, {}); // 得到 { 1: { id: 1, name: 张三 }, 2: { id: 2, name: 李四 } }再比如去重const nums [1, 2, 2, 3, 3, 4]; const unique nums.reduce((acc, num) { if (!acc.includes(num)) acc.push(num); return acc; }, []); // 得到 [1, 2, 3, 4]因为acc可以是任意类型所以reduce的发挥空间非常大。但我也要说句实在话不要为了秀技术把所有逻辑塞进reduce。业务代码里一个filter加一个map就能读懂的逻辑没必要硬拗成一个复杂的reduce。可读性永远排第一。3.2 every 与 some一个都不能少一个就够了every和some是一对“判定搭档”返回的都是布尔值适合放在条件判断里。every要求数组里每一个元素都满足条件只要有一个不满足立刻返回false。some要求数组里至少有一个元素满足条件找到一个满足的立刻返回true。const scores [88, 92, 75]; const allPass scores.every(score score 60); // true const hasTop scores.some(score score 90); // true这两个方法有一个共同的特性短路求值。every在遇到第一个不满足的元素时就会停止遍历some在遇到第一个满足的元素时就会停止遍历。所以它们不止是“判定”还自带性能优化。有一个容易被误用的细节对空数组调用这两个方法时every返回truesome返回false。很多人觉得这不符合直觉但从逻辑学角度其实是合理的——空数组中“不存在”不满足条件的元素所以全员通过同时也“不存在”满足条件的元素所以没有命中。我举个例子帮你记女朋友问你“衣柜里的衣服都是新的吗”如果衣柜是空的这个问题没法证伪只能算“都是新的”everytrue。如果问你“有能穿出门的吗”空衣柜里当然没有somefalse。这么一联想就记住了。3.3 find查到就停比filter更“省油”find返回第一个满足条件的元素本身找不到返回undefined。它和filter最大的区别是filter会找出所有符合条件的元素而find只要找到一个就立刻停下来。const students [ { name: 小明, score: 80 }, { name: 小红, score: 95 }, { name: 小刚, score: 90 }, ]; const firstTop students.find(student student.score 90); // 得到 { name: 小红, score: 95 }所以当你只需要“第一个满足条件的项”时优先用find别用filter()[0]。后者不仅语义不够准确还会遍历完整数组白白浪费性能。数据量小看不出差别一旦数组里有几千几万个对象性能对比就很明显了。find的坑主要在“找不到时返回undefined”。如果你的业务依赖这个结果继续往下走一定要做空值判断否则可能出现类似Cannot read properties of undefined的报错。这个我在第5章会继续讲。4. 实战案例七个方法串进一个真实业务场景光讲单个方法的用法你可能会觉得抽象。我拿一个比较完整的业务场景出来演示一下怎么把这七个方法组合起来解决实际问题。4.1 场景描述购物订单的金额汇总与状态判断假设我们有一个购物车。每个商品有名字、单价、数量、是否已勾选等字段。我们需要做四件事找出所有被勾选的商品。把被勾选的商品按“单价 × 数量”计算出小计金额。汇总所有被勾选商品的总价。判断所有被勾选商品是否都属于“生鲜类”以及是否至少有一个商品价格超过50元。找到第一个库存不足的商品。这类需求在电商后台、订单结算页里非常典型。我用七个方法分别实现其中不同的环节给大家看看组合拳的威力。4.2 完整实现与代码注释解读先定义模拟数据const cart [ { name: 苹果, category: 生鲜, price: 12, count: 3, stock: 100, checked: true }, { name: 牛奶, category: 饮品, price: 58, count: 2, stock: 50, checked: true }, { name: 薯片, category: 零食, price: 6, count: 10, stock: 8, checked: false }, { name: 牛肉, category: 生鲜, price: 85, count: 1, stock: 20, checked: true }, { name: 鸡蛋, category: 生鲜, price: 15, count: 2, stock: 0, checked: true }, ];然后逐个实现业务需求。筛选出被勾选的商品用filterconst checkedItems cart.filter(item item.checked); // 得到苹果、牛奶、牛肉、鸡蛋这4条数据计算每个勾选商品的小计金额用mapconst subtotals checkedItems.map(item ({ name: item.name, subtotal: item.price * item.count, })); // 得到 [{ name: 苹果, subtotal: 36 }, { name: 牛奶, subtotal: 116 }, ...]汇总总价用reduceconst totalPrice subtotals.reduce((sum, item) sum item.subtotal, 0); // 结果是 36 116 85 30 267其实第二步和第三步可以合并成一个reduce直接用checkedItems累加const totalPrice2 checkedItems.reduce((sum, item) sum item.price * item.count, 0);我在实际开发里通常会合并因为不需要中间态时没必要多遍历一次。上面的分开写法只是为了展示reduce和map各自的用法大家看需求取舍。判断勾选商品是否全是生鲜用everyconst allFresh checkedItems.every(item item.category 生鲜); // 牛奶是饮品所以结果是 false判断是否存在价格超过50元的勾选商品用someconst hasExpensive checkedItems.some(item item.price 50); // 牛奶58、牛肉85结果是 true找到第一个库存不足的勾选商品用findconst lowStockItem checkedItems.find(item item.stock item.count); // 鸡蛋的库存0小于数量2所以返回 { name: 鸡蛋, ... }到这里五个方法已经全部用上。最后一个forEach用在哪里通常是在把结果渲染到界面上或者写日志的时候subtotals.forEach(item { console.log(${item.name} 的小计是 ${item.subtotal} 元); });4.3 当初我为什么这样选型可读性优先、兼顾性能这段代码如果硬要用传统的for循环写当然也能写但会多出很多中间变量和if条件分支。用数组方法的核心优势是每一个方法名本身就是语义。别人看代码的时候看到filter就知道是在筛选看到reduce就知道是在汇总根本不需要逐行读逻辑。不过我也要说一个反面教训。曾经我见过一段生产代码把reduce的回调写得特别长里面嵌套了三层if还夹着map和filter看起来是“一行代码搞定一切”实际上花了我半小时才读懂。最后我重构成了三段普通逻辑问题瞬间清晰。所以我的经验是方法选型要以语义清晰为第一标准不要为了“纯函数式风格”牺牲阅读体验。5. 高频问题与排查技巧90%的前端都踩过这些坑这一章是全文最“值钱”的部分。我把开发中真实遇到过的高频问题整理成速查表每个都附排查思路和解决办法。5.1 想在forEach里break为什么不行三个替代方案先明确结论forEach不支持break也不支持return false来终止循环。这是它的设计约束不是 bug。替代方案有三个改用for...of循环里面用break直接跳出。改用some方法在条件满足时返回true遍历自然终止。// 找到大于3的数字就停止并把结果打印出来 [1, 2, 3, 4, 5].some(num { if (num 3) { console.log(num); return true; // 返回 true 后 some 立刻停止遍历 } });改用find或findIndex找到目标元素即返回。还有一个很巧的办法用try...catch包裹forEach在回调里 throw 一个自定义对象来中断。但这种写法把异常当成流程控制用可读性差我不推荐日常使用。除非是面试题里被问到否则老老实实用前面三种方案。5.2 空数组、稀疏数组与空值处理这里有个很值得说的细节。数组方法对“空位”的处理方式不一样forEach、map、filter、reduce会跳过稀疏数组中的空位。every、some、find也会跳过空位。但Array.prototype.forEach不会跳过值为undefined的元素。我举个例子const sparse [1, , 3]; // 中间是空位不是 undefined sparse.forEach(item console.log(item)); // 打印 1 和 3不会打印空位 const withUndefined [1, undefined, 3]; withUndefined.forEach(item console.log(item)); // 打印 1, undefined, 3这个差异常被忽略。如果你在遍历一个从接口返回的数组里面恰好有空位你的map可能会产生预期外的数组结构。不过在现代框架中数据一般会经过序列化空位很少出现。真遇到时建议先用Array.from或者扩展运算符把稀疏数组转成正常数组再执行遍历const normal [...sparse]; // 空位会被转成 undefined normal.map(item item item * 2);5.3 链式调用的性能与可读性平衡很多人喜欢写这样的链式调用const result arr .filter(item item.price 10) .map(item item.price * 2) .reduce((sum, price) sum price, 0);这种写法读起来很舒服逻辑也清晰。但有一个性能层面的缺点filter会生成一个新数组map又会再生成一个新数组。如果原始数组有几万条数据中间数组的内存开销和多次遍历的耗时都是真实存在的。性能要求不高的管理后台类项目我建议无脑用链式写法维护性优先。但如果确定是高频执行路径比如每秒触发多次的搜索、过滤逻辑可以考虑用单次reduce合并逻辑或者直接改for循环。判断标准很简单先用可读性强的写法线上遇到性能瓶颈再去优化。不要提前优化也不要盲目优化。5.4 回调里返回值细节map和filter最容易翻车再强调一遍最容易翻车的两个点map回调没有return结果数组全是undefined。filter回调返回了非布尔值但想表达“等于某个条件”时结果和预期不符。我提供一个自检小技巧。写完后在控制台看一下数组长度map的结果长度必须和原数组一致一旦少了或多了先怀疑是不是用了别的逻辑改变了结构filter的结构长度可长可短但如果出现预期外的undefined多半是回调里的条件表达式写反了。常见报错与排查方向报错信息常见原因排查方向SyntaxError: Illegal break statement在forEach回调里写break改用for...of/some/findTypeError: Reduce of empty array with no initial value空数组调用reduce且未提供初始值给reduce补初始值TypeError: Cannot read properties of undefined (reading x)find未找到元素后续直接访问属性先判空再访问map结果出现大量undefined回调体没写return检查回调体是否有返回值我在真实项目里排查“Cannot read properties of undefined”的次数特别多十有八九都是find或findIndex返回-1或undefined后后面直接拿结果继续用导致的。聊到这儿该说的坑基本都点到了。最后分享一个我自己的小习惯不管用哪个数组方法我都先问自己一个问题——我到底需要一个新数组、一个单值、一个布尔值还是一个元素想清楚这一点方法名字自己就跳出来了。写代码这么多年数组遍历用得好不好关键不在于背了多少API而在于拿到需求时能不能一眼分辨出它属于哪一类问题。你今天把这七个方法按“遍历派、变换派、筛选派、聚合派、判定派、查找派”记好下次写业务的时候脑子里这把尺子自然会帮你做选择。
网站建设高端定制企业官网