1. 数组循环,不止是“遍历一遍”
JavaScript里的数组循环,是每个写前端的人几乎每天都要碰的东西。但你可能也发现了:同一个数组,有的人用for,有的人用forEach,有的人甩出一行map就完事。写法千奇百怪,性能、可读性、踩坑概率也完全不一样。
这篇内容不打算讲那种“这是for,这是while,大家跟我读一遍”的入门教程。我更想聊的是:数组循环背后到底有哪些门道,不同场景下应该怎么选,以及我在真实项目里踩过的那些坑。无论是刚入行不久、还在纠结用哪种循环的初学者,还是已经写了两三年、想系统梳理一下的老手,这篇文章应该都能给你点东西。
先给个结论放这儿:数组循环没有绝对的“最佳写法”,只有“当前场景下最合适的写法”。但如果你能把各种循环方式的底层行为和适用边界都搞清楚,写出来的代码既快又不容易出bug,那就是真正吃透了这东西。
2. 先想清楚:你手里的是数组,还是“看起来像数组的东西”
2.1 数组的本质是连续内存加原型方法
数组在JavaScript里是个很有意思的存在。它不像C语言里那种严格意义上的“连续内存块”,更像是一个经过特殊处理的对象,key是数字索引,value是元素,同时继承了一个Array.prototype原型,上面挂着一堆方法:map、filter、forEach、reduce、some、every……这些方法本质上都是在帮你做“循环”这件事,只是各自有不同的返回值和处理逻辑。
这也就解释了为什么“循环数组”这件事会被拆成这么多写法:你每次选循环方式,实际上是在选一种处理语义。比如map的语义是“逐个转换并返回新数组”,filter的语义是“挑出符合条件的组成新数组”,forEach的语义是“我只要副作用,不要返回值”。语义选对了,代码读起来就像在表达业务逻辑,而不是在描述机器步骤。
2.2 类数组对象和可迭代对象是两回事
很多人写循环写出bug,问题往往不在循环本身,而在循环的对象。比如document.querySelectorAll返回的NodeList,它虽然有length、也能用索引访问,但它不是Array实例,直接调用forEach在老版本浏览器里会报错。arguments对象也是这样,函数里的arguments是个类数组,没有数组的方法。
这时候你可能有三种处理方式:第一种是Array.prototype.slice.call(arguments)把它转成真数组;第二种是Array.from(arguments),这是ES6提供的更直观的写法;第三种是直接用展开运算符[...arguments]。这三种方式都能完成转换,但适用场景略有不同。Array.from最强的地方在于它还能接受第二个参数——一个map函数,相当于转数组的同时做一次遍历处理,一步到位。
另外一个容易混淆的概念是“可迭代对象”。只要对象有Symbol.iterator方法,它就是可迭代的,可以用for...of遍历。数组本身是可迭代的,字符串也是,Set、Map也是。但普通对象不是,所以不能用for...of直接遍历对象。这也是很多初学者写for...of对象报错的根本原因。
3. 选型之前,先看清每种循环的“脾气”
3.1 for循环:最原始,也最可控
先说最基础的for循环。很多老程序员对数组方法有偏见,觉得花里胡哨不如for直接。但for循环最大的价值不是性能,而是可控性。你可以随时break跳出,可以continue跳过本次,可以自由控制索引的增减步长,甚至可以在循环体里修改索引来实现复杂的跳跃逻辑。
但for循环的缺点也特别明显:容易写出“差一错误”,也就是边界判断不对。比如for (let i = 0; i <= arr.length; i++)这种经典的越界写法,最后拿到的arr[i]是undefined。另外,for循环本质上是“命令式”代码,读代码的人需要自己在大脑里模拟一遍执行过程,才能理解这段循环在干什么。
3.2 for...of循环:既干净又能跳出
ES6引入的for...of循环是我个人非常喜欢的一种方式。它不像传统for那样要手动管理索引,也不像forEach那样不能break。它直接遍历“值”,写法简洁,语义清楚。
const arr = [12, 25, 8, 30, 17]; for (const item of arr) { if (item > 20) { console.log(`第一个大于20的是:${item}`); break; } }看到没,需要提前终止的时候,for...of比forEach舒服太多了。而且for...of不仅能遍历数组,还能遍历Set、Map、生成器等所有可迭代对象,通用性很强。
3.3 forEach:只管干活,不管中断
forEach是数组方法里最常用的一个。它的好处是代码看起来干净,不会出现索引那种“噪音”。但它有几个非常反直觉的坑,很多人掉进去过:
第一个坑:forEach里不能用break跳出循环。你写了break,直接报语法错误。想提前结束,只能抛异常,但这显然不是正常业务的写法。
第二个坑:forEach里return不会终止循环,只是跳过当前这一次回调。很多从其他语言转过来的朋友以为return相当于“退出函数”,但在forEach里它只是说“本次处理到此为止,继续下一个”。
第三个坑:forEach遍历过程中如果修改了数组元素,回调里看到的值可能和你预期不一致。这个后面细说。
第四个坑:forEach没法很方便地拿到当前索引以外的信息。其实它是有索引参数的,回调函数第二个参数就是index,但很多人写的时候总是忽略它。
const list = ['a', 'b', 'c']; list.forEach((item, index, array) => { console.log(index, item, array); });回调里第三个参数甚至能拿到原始数组的引用。但注意,如果你在回调里push了新元素,forEach是会遍历到新增元素的,这就可能导致意外行为。
3.4 map/filter/reduce:声明式循环
map、filter、reduce这三兄弟是函数式编程在JavaScript里的主力。它们的共同点是不直接操作原始数据,而是返回一个新结果。map做“一对一转换”,filter做“筛选”,reduce做“归约”。
const prices = [100, 200, 300]; const withTax = prices.map(price => Math.round(price * 1.13)); const affordable = prices.filter(price => price <= 200); const total = prices.reduce((sum, price) => sum + price, 0);这类写法的好处是代码极其简洁,而且意图自描述。看到map你就知道是转换,看到filter你就知道是筛选。坏处是如果回调函数里写的逻辑太复杂,反而会变得难以阅读。我的建议是:回调逻辑超过三行,就该抽成具名函数,而不是堆一大坨箭头函数。
3.5 some/every/find:带判断的循环
some表示“有没有至少一个满足条件”,every表示“是不是全都满足”,find表示“找出第一个满足条件的元素”。这三个方法都能提前终止遍历,而且语义非常清晰,能用它们的地方就别自己写for循环去判断了。
const users = [{name: '张三', age: 18}, {name: '李四', age: 25}]; const hasAdult = users.some(user => user.age >= 18); const allAdult = users.every(user => user.age >= 18); const firstAdult = users.find(user => user.age >= 18);配合可选链和箭头函数,这三行代码能把原来的一大段for循环逻辑压到一行,而且读起来像英文句子一样顺畅。我自己在Code Review时看到有人用for循环去“找有没有某个元素”,都会建议换成some或find——不是为了炫技,是这种写法的边界情况更少,不容易写错。
4. 实操过程中最容易被忽略的细节
4.1 遍历时修改数组,后果比你想的严重
先说一个真实踩过的坑。有一次我处理一个列表,需求是“把符合条件的元素删掉”。我当时想都没想就写了:
const arr = [1, 2, 3, 4, 5]; arr.forEach((item, index) => { if (item % 2 === 0) { arr.splice(index, 1); } }); console.log(arr); // [1, 3, 5]?还是别的?结果是[1, 3, 5],看起来好像对了。但如果换一组数据:
const arr = [1, 2, 3, 4, 5, 6]; arr.forEach((item, index) => { if (item % 2 === 0) { arr.splice(index, 1); } }); console.log(arr); // 实际结果是 [1, 3, 5, 6]看到了吗?6没有删掉。原因是splice会改变数组长度和索引,forEach遍历使用的索引是单调递增的,删除元素后,后面的元素往前挪了一位,但循环索引已经过去了,等于“跳过了”一个元素。
正确的做法是:要么用filter重新生成数组,要么倒序遍历删元素。
// 推荐:filter生成新数组 const result = arr.filter(item => item % 2 !== 0); // 或者:倒序遍历,避免索引偏移 for (let i = arr.length - 1; i >= 0; i--) { if (arr[i] % 2 === 0) { arr.splice(i, 1); } }这条经验值多少钱?至少值一次线上bug修复的时间。你在循环过程中对数组做结构性修改(push、pop、splice、sort)时,一定要想清楚索引会不会乱掉。
4.2 异步循环:forEach的“坑”是同步的“假循环”
另一个高频问题是在循环里做异步操作。比如你要循环上传文件、循环发请求、循环查数据库,很多人很自然地写成:
const ids = [1, 2, 3, 4, 5]; ids.forEach(async (id) => { await fetch(`/api/data/${id}`); });然后发现:请求确实是发出去了,但后面的代码没法等所有请求完成。因为forEach不会等待异步回调执行完毕,它只是把所有回调一股脑丢出去就完了。async在这里只是把forEach变成了“并发发起异步任务”,而不是“逐个等待”。
这种场景下,有几种选择:
- 用for...of配合await,串行执行,逻辑最清晰;
- 用Promise.all配合map,并发执行;
- 用reduce配合Promise,串行执行且能传递结果。
// 串行:一个个来 for (const id of ids) { await fetch(`/api/data/${id}`); } // 并发:同时发出去,全部完成再继续 await Promise.all(ids.map(id => fetch(`/api/data/${id}`)));如果你要控制并发数量,不能用Promise.all全量并发(可能导致服务器被打挂),那可以自己写个带并发限制的工具函数,或者直接去找p-limit这类现成的库。但原理上,for...of+await这条组合拳是最能在性能和逻辑清晰度之间取得平衡的。
4.3 break、return、continue在循环里的“行为地图”
很多初学者对break、return、continue在不同循环方式里的表现非常困惑。这里我整理一个速查表:
| 循环方式 | break | continue | return |
|---|---|---|---|
| for | 跳出整个循环 | 跳过本次,继续下一次 | 退出整个函数 |
| for...of | 跳出整个循环 | 跳过本次,继续下一次 | 退出整个函数 |
| forEach | 语法错误 | 跳过本次回调 | 只结束当前回调,循环继续 |
| map/filter/reduce | 语法错误 | 语法错误 | 只结束当前回调 |
| some/every/find | 相当于找到结果自动停止 | 返回true继续判断 | 只结束当前回调 |
看到规律了吗?能在外部控制循环流程的只有for和for...of,数组方法内部完全有一套自己的逻辑。所以如果你的需求里有“找到就停”“不满足就提前结束”这类条件,优先考虑for...of,或者some/find这类能提前终止的方法。
4.4 对象不是数组,别硬循环
循环一个普通对象的时候,最常见的姿势是for...in。但for...in有个坑:它会遍历到原型链上可枚举的属性。如果不小心,你会拿到一些莫名其妙的东西。所以用for...in遍历对象时,最好配合hasOwnProperty判断:
const obj = { a: 1, b: 2 }; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { console.log(key, obj[key]); } }更现代一点的做法是用Object.keys、Object.values、Object.entries来解决:
Object.entries(obj).forEach(([key, value]) => { console.log(key, value); });Object.entries返回的是[key, value]组成的二维数组,然后你对这个二维数组做循环,想怎么遍历都行。这样既绕开了原型链污染问题,又能解锁数组的所有方法。
5. 动手撸一个“带终止条件的forEach”
5.1 需求分析
前面说了forEach不能break。但实际业务里很多场景是:我要遍历数组,处理每一个元素,一旦满足某个条件就停止后续处理。你当然可以用for...of,但有时候项目代码里全面采用了函数式风格,或者你想封装一个通用的工具方法,那就可以自己实现一个“可终止的forEach”。
这种需求在真实项目中很常见。比如你有一个批量任务列表,每个任务执行起来很昂贵,一旦发现某个任务失败,就立即停止后续任务的执行。这时候内置的forEach显然是撑不住的。
5.2 代码实现
function forEachBreakable(arr, callback) { for (let i = 0; i < arr.length; i++) { const result = callback(arr[i], i, arr); // 回调返回 false,就终止循环 if (result === false) { break; } } } // 使用示例 const tasks = [ { name: '任务A', ok: true }, { name: '任务B', ok: false }, { name: '任务C', ok: true }, ]; forEachBreakable(tasks, (task, index) => { console.log(`开始执行:${task.name}`); if (!task.ok) { console.log(`任务失败,停止后续执行(索引 ${index})`); return false; } });这个实现的原理其实特别简单:forEach做不到break,是因为它的回调返回值没有被外部循环所利用。我们自己写一个for循环作为底层驱动,然后把“是否继续”的判断权通过返回值暴露给调用者。回调返回false就break,否则继续。这就相当于既享受了forEach的写法简洁,又保住了for循环的控制力。
5.3 更进一步:让回调支持async/await
如果你需要在这套工具里支持异步任务,比如“串行执行异步操作,一旦失败立即停止”,那只需要把底层循环改成for...of,并await回调的返回值:
async function forEachBreakableAsync(arr, callback) { for (const [index, item] of arr.entries()) { const result = await callback(item, index, arr); if (result === false) { break; } } }这里用arr.entries()是为了拿到[index, item]这种结构,然后用数组解构语法一次取出两个变量。两个版本的差异在于:同步版本用for循环就够了,异步版本必须用for...of加上await,否则无法保证串行执行。
5.4 为什么不用for...in
顺带提一句,遍历数组的时候永远不要用for...in。虽然它也能拿到索引:
const arr = ['a', 'b']; for (const index in arr) { console.log(index, arr[index]); }但这里拿到的index是字符串,不是数字,而且for...in遍历的是可枚举属性,如果有谁往Array.prototype上添加了自定义方法,for...in会把它们也遍历出来。网上很多“老代码”这么写,但放到现代工程里完全不应该出现。
6. 性能对比:到底谁更快
6.1 实测结果
很多人关心性能,我直接说结论:在绝大多数业务场景下,for循环和数组方法的性能差异完全可以忽略,你根本不需要为性能去选循环方式。真正影响性能的是循环体内部做了什么,而不是循环本身。
但为了严谨,还是做个简单的性能对比。假设一个长度10万、元素是数字的数组,分别用for、for...of、forEach、map做累加或转换。在最新版Chrome里,趋势大概是这样:
- for循环最快,因为它几乎没有任何额外开销;
- forEach比for慢一点点,但差距通常在个位数百分比;
- for...of比forEach再慢一点,因为迭代器协议有额外开销;
- map在纯转换场景下和forEach差不多,但它会创建一个新数组,内存开销更大。
这个测试结果在不同浏览器、不同机器上有波动,但整体排序基本稳定。如果你的循环数量级在几千几万,这点差异根本感知不到。
6.2 真正要担心的内存问题
真正需要警惕的是map这类方法会产生新数组。如果你在循环里嵌套循环,再用map生成新数组,又用filter生成另一个新数组,数据量大时内存占用会成倍上涨。比如:
const bigData = Array.from({ length: 100000 }, (_, i) => i); const result = bigData .filter(n => n % 2 === 0) .map(n => n * 10) .reduce((acc, n) => acc + n, 0);这段代码虽然简洁,但filter先生成了一个5万长度的新数组,map又生成了一个5万长度的新数组,最后才reduce。如果在内存受限的环境里跑大批量数据,可以考虑用for循环一次搞定,省掉中间数组的开销。
6.3 过早优化是万恶之源
写代码的顺序建议是:先保证逻辑正确、可读性好;遇到真实的性能瓶颈,用performance工具定位到具体代码,再针对性地优化循环写法。不要一开始就因为“听说for最快”把所有map都改成for循环,那是典型的“过早优化”。要知道,for循环写多了,出边界bug的概率也更大,省下来的那几毫秒还不够调试费的时间成本。
7. 数组去重、数组转字符串等高频循环场景实战
7.1 去重:Set一行搞定,但要看场景
数组去重几乎是循环场景里出场率最高的问题。最简洁的做法是利用Set的特性:
const arr = [1, 2, 2, 3, 3, 4]; const unique = [...new Set(arr)]; console.log(unique); // [1, 2, 3, 4]但这里有个细节:Set的去重是基于严格相等(SameValueZero)的,也就是说1和'1'会被当成不同值,两个对象即使结构完全一样也不会被去重。如果你要去重的是一个对象数组,那Set就无能为力了。比如:
const users = [ { id: 1, name: '张三' }, { id: 1, name: '张三' }, ]; const uniqueUsers = [...new Set(users)]; // 长度还是2,因为引用不同这种情况你需要基于某个唯一标识去重,比如按id:
const seen = new Set(); const uniqueById = users.filter(user => { if (seen.has(user.id)) { return false; } seen.add(user.id); return true; });这里用filter配合Set的“查重+标记”模式,本质上还是一次遍历,但逻辑很清晰。filter回调只在seen里没有该id时返回true,同时把id加到seen里,这样后出现的重复项就被过滤掉了。
7.2 数组转字符串:join远比循环拼接靠谱
把数组转成字符串,大家可能第一反应是用循环拼接。比如:
const arr = ['a', 'b', 'c']; let str = ''; for (let i = 0; i < arr.length; i++) { str += arr[i]; if (i < arr.length - 1) { str += ','; } }但数组本身提供了join方法,一行就搞定了:
const str = arr.join(',');join底层是用C++实现的(在V8引擎里),性能比JavaScript层的手工字符串拼接好太多。而且join支持传入任意分隔符,不传默认用英文逗号。如果数组里有嵌套数组,要先拍平(flat或flatMap),否则会拼出“1,2,3,4”这种带嵌套结构的字符串,注意区分。
7.3 二维数组和矩阵操作
前面热词里出现了“二维数组”和“C语言二维数组”相关的问题,JavaScript里二维数组其实就是一个数组套数组。循环遍历二维数组最常见的就是双重for循环:
const matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ]; for (let row = 0; row < matrix.length; row++) { for (let col = 0; col < matrix[row].length; col++) { console.log(`matrix[${row}][${col}] = ${matrix[row][col]}`); } }如果想把二维数组拍平成一维数组,ES2019提供了flat方法:
const flat = matrix.flat(); // [1, 2, 3, 4, 5, 6, 7, 8, 9]flat默认只拍平一层,如果需要深度拍平,传Infinity。但注意,flat把空位也去掉了,这个行为在某些场景下要注意。
7.4 稀疏数组:forEach会跳过空位
JavaScript数组允许“空位”(holes),比如:
const arr = [1, , 3]; console.log(arr.length); // 3 console.log(arr[1]); // undefined这个arr[1]位置不是undefined值,而是一个真实的“空位”。区别在于:forEach、map、filter等数组方法遍历时会跳过空位,但for循环不会跳过。所以如果你用map处理这个数组:
const result = arr.map(item => item * 2); console.log(result); // [2, 空位, 6]结果是保留了空位。但如果你用for循环,读到arr[1]会得到undefined,再乘2就是NaN。这个差异非常隐蔽,我在实践里就遇到过一次:一个接口返回的数组里有空位,前端用map生成新数组,渲染出来的列表中间少了一块。排查了半天才发现是空位问题。后来养成了习惯:从接口拿到的数据,先过滤一下空位:
const cleaned = arr.filter(item => item !== undefined);注意,用filter过滤空位和undefined是两回事,filter会跳过空位,也会处理值为undefined的元素。具体业务里可以用item != null这种宽松判断,把null和undefined都滤掉,前提是你确认业务上不需要这两个值。
8. 常见问题速查表与避坑指南
8.1 高频报错与行为异常
我整理了一份我在实际项目中遇到的问题清单,每一行都是一个真实踩过的坑:
| 表现 | 根本原因 | 解决方案 |
|---|---|---|
| forEach里写break报语法错误 | forEach不支持break | 改用for...of或some |
| 循环里splice删元素后漏项 | 索引偏移 | 倒序遍历或改用filter |
| for...in遍历数组拿到字符串索引 | for...in的语义问题 | 改用for...of |
| map回调里改了原数组 | map的设计是返回新数组 | 确认意图,别依赖副作用 |
| 循环里发异步请求,结果顺序错乱 | 并发执行而非串行 | for...of+await或Promise.all |
| 大数组map后内存暴涨 | 生成了多个中间数组 | 改用for循环一次处理 |
| 数组有稀疏空位,map结果不对 | 数组方法跳过空位 | 先过滤空位 |
8.2 forEach里的索引参数到底怎么用
forEach的回调函数有三个参数:当前元素、当前索引、数组本身。很多人只用第一个参数,但索引在某些场景下非常有用。比如你要把数组渲染成列表,每行的序号就是索引:
const items = ['项目A', '项目B', '项目C']; items.forEach((item, index) => { console.log(`第${index + 1}个:${item}`); });还有一个技巧:通过索引判断是不是最后一个元素,避免在拼接时多加分隔符:
items.forEach((item, index) => { str += item; if (index < items.length - 1) { str += '、'; } });不过这种拼接场景,我更推荐join,这里只是展示索引的实际用途。
8.3 循环变量作用域:var和let的坑
for循环里的声明语句,用var和let完全是两个世界:
// 用var:循环结束后i还在 for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)); // 输出 3, 3, 3 } // 用let:每次循环都是一个新的i for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i)); // 输出 0, 1, 2 }原因很简单:var声明的变量是函数作用域,循环结束后它仍然存在,只是值变成了最终值;let声明的变量是块级作用域,每次迭代都会创建新的绑定,所以闭包捕获到的是当次循环的i。这个知识点在面试题里出现频率极高,在真实项目里也坑过不少人——尤其是你给一组按钮绑定点击事件,点击后取到的i永远等于最后一个值,那一刻你会深刻理解这个坑的痛苦。
8.4 while循环:什么时候用
for循环能干的活,while循环基本都能干,反之亦然。但有些场景while更顺手:
let i = 0; while (i < arr.length) { // 这里可以做更复杂的判断 if (arr[i] === target) { break; } i++; }当你不知道循环次数、只知道终止条件的时候,while更合适。比如从某个数组中不断取出元素直到满足条件。但while有个风险:很容易忘记更新循环变量,导致死循环。我自己的习惯是:能用for就用for,因为for把“初始化、条件、更新”写在了一行里,结构更完整,不容易漏更新。
8.5 生成器与无限数组:循环的边界玩法
如果你的“循环”不只是在已有数组上遍历,而是需要按规则生成序列,那么生成器(Generator)配合for...of会有奇效。比如生成一个斐波那契数列的前N项:
function* fibonacci() { let a = 0, b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } const fib = fibonacci(); for (let i = 0; i < 10; i++) { console.log(fib.next().value); }这里生成器是无限序列,不能直接放进数组方法里遍历,但你可以配合next手动取,或者用for...of配合计数器来取前N项。这种玩法不算日常高频场景,但了解它对理解“循环”的本质很有帮助——循环不一定是在“遍历已经存在的东西”,也可以是在“按需生成东西”。
8.6 实战里的一个综合案例
最后给你一个我最近实际写过的场景,把今天聊的几个点串起来。需求是:有一个订单列表,每个订单有商品列表,要把所有订单里金额大于100的商品筛选出来,并按销售额从高到低排序。
const orders = [ { id: 1, items: [{ name: '键盘', price: 88 }, { name: '显示器', price: 1200 }] }, { id: 2, items: [{ name: '鼠标', price: 45 }, { name: '键盘', price: 58 }] }, { id: 3, items: [{ name: '耳机', price: 150 }] }, ]; const expensiveItems = []; for (const order of orders) { for (const item of order.items) { if (item.price > 100) { expensiveItems.push(item); } } } expensiveItems.sort((a, b) => b.price - a.price); console.log(expensiveItems);这里我用for...of嵌套遍历订单和商品,是因为需要在前一层的每一笔订单里逐项做判断,这种场景如果用flatMap加filter也能写:
const expensiveItems = orders .flatMap(order => order.items) .filter(item => item.price > 100) .sort((a, b) => b.price - a.price);两种写法结果一样,但第二种更“声明式”,一行表达一个步骤,逻辑链条非常清晰。flatMap把嵌套结构拍平成一维,filter做条件筛选,sort做排序。如果你的项目里其他人都习惯函数式写法,用第二种会让你代码风格更统一;如果团队里新手多,第一种更直白、容易打断点调试。没有绝对的对错,关键是团队风格一致。
9. 最后分享一个我自己养成的习惯
我在代码评审里经常看到有人纠结要不要为了“优化”把forEach改成for循环,或者把map改成for循环push。说实话,除非你的项目要做那种数据密集型运算(比如处理几十万条数据的表格,或者做canvas像素级操作),否则真的没必要纠结这一点点性能差异。我做了这么久前端,真正因为循环写法导致性能问题的项目屈指可数,反而因为循环逻辑写错导致线上bug的次数多得多。
所以我现在定了一个简单的选择原则:先考虑语义是否匹配,再考虑可读性,性能放在最后考虑。要转换数组用map,要筛选用filter,要归约用reduce,要中断用for...of,要索引控制用for。这个原则帮我解决了很多选择困难,也让我的代码在可读性和健壮性上都稳定了不少。
还有一个建议:如果你常用的数组方法里有什么API记不住,别怕,查文档不丢人。用得多了自然就熟了。真正值钱的是你对这个语言数据结构底层行为的理解——数组是对象、索引是属性、循环是控制流、遍历是语义。把这几个点串起来,你看待JavaScript数组循环的视角就会完全不同。