资讯动态

JavaScript数组遍历完全指南:从底层原理到forEach、map、reduce实用技巧

发布时间:2026/9/12 7:48:30 来源:尧图企业网站定制
写 JavaScript 写了这么多年数组遍历应该算是我每天都要碰的基础操作了。但说实话市面上绝大多数教程都把数组遍历讲得太浅翻来覆去就是forEach和map的区别连for...of都只是提一嘴。真正到写复杂业务的时候很多人要么只会forEach一把梭要么用错了遍历方式导致性能问题要么在遍历过程中踩了修改原数组的大坑。这篇文章想一次性把 JavaScript 数组遍历这件事讲透——从底层的循环原理到函数式遍历家族再到迭代器机制最后配上实战场景和性能数据。不管你是刚入门的前端新人还是写了几年业务的老手这篇内容都能帮你把遍历这件事从“会写”提升到“写对”。1. 数组遍历的底层逻辑先搞懂数组到底是什么1.1 数组不是你以为的数组很多人一上来就背遍历方法忽略了最根本的底层机制。JavaScript 里的数组本质上并不是传统意义上的连续内存空间它是一个特殊的对象键名是“0”、“1”、“2”这样的字符串索引底层通过Array构造器创建并维护了一个length属性。const arr [a, b, c]; // 等价于一个对象 // { 0: a, 1: b, 2: c, length: 3 }这个理解很重要因为它直接决定了遍历行为。比如下面这个数组const sparse []; sparse[0] a; sparse[3] d; console.log(sparse.length); // 4 console.log(sparse); // [ a, 2 empty items, d ]中间有两个空槽位。不同遍历方式对空槽位的处理完全不同for循环会输出undefinedforEach、map、filter会直接跳过空槽位而for...of会输出undefined。这是实际开发中极易踩坑的地方后面我会专门讲。1.2 最基础的 for 循环永远不要轻视虽然函数式遍历很香但我必须说传统的for循环在某些场景下依然是最优解。它的优势有三点可以精确控制步长、可以提前break、可以反向遍历。const arr [1, 2, 3, 4, 5]; // 正序遍历 for (let i 0; i arr.length; i) { console.log(arr[i]); } // 反向遍历 for (let i arr.length - 1; i 0; i--) { console.log(arr[i]); } // 跳跃遍历每隔一个 for (let i 0; i arr.length; i 2) { console.log(arr[i]); }还有一种写法是把arr.length缓存起来在旧版本引擎上有微弱的性能优势for (let i 0, len arr.length; i len; i) { // do something }现在的 V8 引擎优化已经很好了缓存不缓存差异不大但这是一个好习惯。另外如果你需要同时操作两个数组的对应元素for循环也是最直观的const arr1 [1, 2, 3]; const arr2 [10, 20, 30]; for (let i 0; i arr1.length; i) { console.log(arr1[i] arr2[i]); // 11, 22, 33 }1.3 while 和 do...while处理不确定次数遍历while循环在数组遍历中用得相对少但在某些“直到满足条件才停止”的场景里很好使。比如从数组中不断取出元素直到为空const queue [1, 2, 3, 4, 5]; while (queue.length) { const item queue.shift(); console.log(item); // 依次输出 1,2,3,4,5 }do...while的特点是无论如何至少执行一次循环体。在实际数组遍历中它很少用但如果你要实现一个“先处理再判断是否继续”的逻辑它就派上用场了。1.4 数组的 length 属性可以“截断”数组这里插一个很多人不知道的技巧直接修改length可以截断数组而这个特性在遍历中也能玩出花样。const arr [1, 2, 3, 4, 5]; arr.length 3; console.log(arr); // [1, 2, 3]反过来把length设大可以扩展数组空位值会是undefinedconst arr [1, 2, 3]; arr.length 5; console.log(arr); // [1, 2, 3, 2 empty items]这个特性可以用来快速清空数组arr.length 0。但要注意它和arr.splice(0)有区别前者直接改变length属性后者会返回被删除的元素并可能触发数组结构调整在性能上length 0通常更快。1.5 小结底层的遍历机制决定了上层 API 行为理解了数组是对象、length 是可修改的、元素槽位可能为空你就能理解为什么forEach会跳过空槽位而for不会。这些底层细节看似无关紧要但在处理从接口返回的异常数据时可能就是 bug 的根源。下一章我们进入最常用的函数式遍历家族。2. 函数式遍历家族forEach、map、filter 一次讲透2.1 forEach最常用的方法但坑也最多forEach是很多人日常用得最多的遍历方法拿到数组就forEach。它的调用形式是const arr [1, 2, 3]; arr.forEach((item, index, array) { console.log(item, index); });回调接收三个参数当前元素、当前索引、数组本身。forEach有三大坑我逐一说明。坑一无法 break 或 return 中断。这是新手最常见的误区。很多人想提前结束遍历写了return结果发现只是跳过了当前回调循环还在继续。const arr [1, 2, 3, 4, 5]; arr.forEach(item { if (item 3) { return; // 这并不能终止遍历只是跳过当前迭代 } console.log(item); // 输出 1, 2, 4, 5 });要提前结束遍历你需要用for循环、for...of、some或every。具体用哪个取决于你要找什么。坑二稀疏数组跳过空槽位。前面提到过forEach对于数组中的空槽位直接跳过而且回调根本不会被调用。const sparse [1, , 3]; sparse.forEach(item console.log(item)); // 只输出 1 和 3 for (let i 0; i sparse.length; i) { console.log(sparse[i]); // 输出 1, undefined, 3 }如果你的数据可能含有空槽位且你需要按索引处理每一位用forEach要格外小心。坑三遍历过程中修改数组会引发怪异行为。forEach的遍历范围是在第一次调用时就确定的。如果你在回调里往数组末尾追加元素这些新元素不会被遍历到。const arr [1, 2, 3]; arr.forEach(item { if (item 1) { arr.push(4); // 这个 4 不会被遍历到 } console.log(item); // 输出 1, 2, 3 });2.2 map不仅仅是遍历更重要的是映射map的本质是“映射”它遍历数组并返回一个长度相同的新数组新数组的每个元素是回调函数的返回值。const arr [1, 2, 3]; const doubled arr.map(item item * 2); console.log(doubled); // [2, 4, 6] console.log(arr); // [1, 2, 3] 原数组不变很多人会问什么时候用forEach什么时候用map我的判断标准非常简单——你需要回调的返回值吗如果需要用map如果只是为了执行副作用比如打印、修改外部变量用forEach。不要为了统一风格而无脑使用map。如果你用map但丢弃了返回值那就白白创建了一个新数组浪费内存。还有一个map的经典误用场景生成 JSX在 React 里。你确实需要map返回一个新数组来渲染列表这个场景map是完全正确的。map同样会跳过稀疏数组的空槽位但返回的新数组会保留空槽位const sparse [1, , 3]; const result sparse.map(item item * 2); console.log(result); // [2, 1 empty item, 6]2.3 filter筛选出符合条件的元素子集filter会遍历数组把回调返回真值的元素保留下来组成一个新数组返回。它不会修改原数组。const nums [1, 2, 3, 4, 5, 6]; const evens nums.filter(num num % 2 0); console.log(evens); // [2, 4, 6]filter的一个实用技巧是和Boolean构造函数一起用用来去除数组中的假值undefined、null、0、、false、NaNconst messy [0, hello, , undefined, null, false, 42]; const cleaned messy.filter(Boolean); console.log(cleaned); // [hello, 42]注意filter(Boolean)会把数字0也过滤掉。如果你的数据里0是有效值就别这么写。filter也有一个小众用法利用它返回新数组的特性实现快速浅拷贝const copy arr.filter(() true);。但更常规的浅拷贝方式还是arr.slice()或[...arr]。2.4 some、every、find、findIndex属于“查找”而非“遍历”这四个方法虽然也遍历数组但本质上是“谓词查询”它们不会生成新数组而是根据回调的返回值做出判断。some至少有一个元素满足条件就返回true并提前终止遍历。every所有元素都满足条件才返回true遇到第一个不满足的就提前终止。find返回第一个满足条件的元素找不到返回undefined。findIndex返回第一个满足条件的元素的索引找不到返回-1。const users [ { name: 张三, age: 20 }, { name: 李四, age: 17 }, { name: 王五, age: 30 } ]; const hasAdult users.some(user user.age 18); // true const allAdult users.every(user user.age 18); // false const firstAdult users.find(user user.age 18); // { name: 张三, age: 20 } const firstAdultIndex users.findIndex(user user.age 18); // 0some和every的提前终止特性非常有用在处理大数组时能显著减少迭代次数。比如判断一个包含上万个元素的数组是否包含某个特定值用some可能第一次迭代就返回了而用forEach要遍历完整数组才能得出结果。2.5 回调的 thisArg 参数每个方法都有一个隐藏参数很多人不知道forEach、map、filter、some、every、find、findIndex这些方法都接受第二个参数thisArg用来指定回调函数中this的值const context { multiplier: 10 }; const arr [1, 2, 3]; const result arr.map(function(item) { return item * this.multiplier; }, context); console.log(result); // [10, 20, 30]注意这里回调不能是箭头函数因为箭头函数没有自己的this。这个参数在实际工程中不常用大部分时候你用闭包就够了但在写工具函数库或者某些需要面向对象写法的时候会遇到。2.6 函数式遍历的性能实测我在 Node.js 18 环境做了一个简单的基准测试遍历一个长度为 100 万的数组分别执行空操作、求和操作。结果大致如下方法耗时求和约说明for 循环1.0x基准最快for...of1.1x略慢于 forforEach1.2x与 for 差距不大map1.4x因创建新数组有额外开销filter1.5x同上reduce1.3x取决于回调复杂度这个结论日常业务中这几种方式的性能差异可以忽略不计除非你在处理数十万级以上的数据。性能不是选型的第一考量代码可读性才是。但如果你明确要处理超大数组比如十万级以上的数据可视化项目for循环和小步优化就能感觉到差距了。3. 迭代器与 for...of现代 JavaScript 遍历的优雅姿势3.1 什么是迭代器协议ES6 引入的迭代器协议为 JavaScript 对象定义了一种统一的遍历方式。一个对象如果部署了Symbol.iterator方法它就被称为“可迭代对象”。数组、字符串、Set、Map、arguments 对象、NodeList 都是内置的可迭代对象。const arr [a, b, c]; const iterator arr[Symbol.iterator](); console.log(iterator.next()); // { value: a, done: false } console.log(iterator.next()); // { value: b, done: false } console.log(iterator.next()); // { value: c, done: false } console.log(iterator.next()); // { value: undefined, done: true }每次调用next()返回一个对象value表示当前迭代的值done表示迭代是否结束。这套协议是for...of、扩展运算符、解构赋值的底层基础。3.2 for...of 的基本用法和优势for...of是基于迭代器协议的遍历方式用法非常简洁const arr [a, b, c]; for (const item of arr) { console.log(item); }和forEach相比for...of有三大优势。优势一可以break和continue。const arr [1, 2, 3, 4, 5]; for (const item of arr) { if (item 2) continue; if (item 4) break; console.log(item); // 输出 1, 3 }这个优势太实用了。当你需要在遍历中提前退出时for...of是比forEach更优雅的选择。优势二可以结合await做异步遍历。const tasks [task1, task2, task3]; for (const task of tasks) { await task(); // 等待每个任务完成后再执行下一个 }forEach配合async/await有个经典问题forEach不会等待异步回调完成它只是把回调全部压入微任务队列。用for...of就没这个问题它的循环体天然支持await。优势三可以遍历任意可迭代对象。// 遍历字符串 for (const char of hello) { console.log(char); // 逐个字符注意是按码点不是按 UTF-16 单元 } // 遍历 Set for (const item of new Set([1, 2, 3])) { console.log(item); } // 遍历 Map 的键值对 for (const [key, value] of new Map([[a, 1]])) { console.log(key, value); }特别是遍历Map时配合解构语法[key, value]代码非常清爽。3.3 for...in 和 for...of 的本质区别这里必须把for...in单独拉出来批评一下。for...in是 ES5 时代就有的语法它的设计目的是遍历对象的可枚举属性名而不是数组的元素。const arr [a, b, c]; arr.customProp hello; for (const key in arr) { console.log(key); // 输出 0, 1, 2, customProp }看到没有for...in把customProp也遍历出来了。如果你在数组上挂载了自定义属性for...in会把这些属性一起遍历出来。更糟糕的是如果某个库在Array.prototype上扩展了方法for...in也会遍历到它们比如旧时代的 polyfill。所以结论非常明确遍历数组永远不要用for...in。for...in的正确使用场景是遍历普通对象const obj { name: 张三, age: 20 }; for (const key in obj) { if (Object.hasOwn(obj, key)) { console.log(key, obj[key]); } }但即使是遍历普通对象我更推荐用Object.keys()配合for...ofconst obj { name: 张三, age: 20 }; for (const key of Object.keys(obj)) { console.log(key, obj[key]); }这样更安全因为Object.keys()只返回对象自身的可枚举属性不包含原型链上的属性。3.4 扩展运算符和解构赋值的隐藏遍历逻辑扩展运算符和解构赋值在遍历横向场景中有着广泛应用它们的底层也是迭代器协议。// 数组合并 const a [1, 2]; const b [3, 4]; const combined [...a, ...b]; // [1, 2, 3, 4] // 复制数组 const copy [...a]; // 将 Set 转数组 const set new Set([1, 2, 3]); const setArr [...set]; // [1, 2, 3] // 字符串转字符数组 const chars [...hello]; // [h, e, l, l, o] // 解构赋值 const [first, second, ...rest] [1, 2, 3, 4, 5]; console.log(first, second, rest); // 1 2 [3, 4, 5]特别注意[...hello]和hello.split()的区别。split()是按 UTF-16 编码单元切割而扩展运算符是按码点迭代。对于包含特殊字符的字符串比如 emoji 或生僻字两者结果可能不同const emoji ; console.log([...emoji]); // [] console.log(emoji.split()); // [\uD83D, \uDE00]虽然两种方式都能work但在处理带复杂字符的字符串时[...emoji].length的语义更符合“真实字符数”的直觉。3.5 类数组转数组这是遍历的经典应用场景函数内部的arguments对象、DOM 里的NodeList、字符串这些都不是真正的数组但它们可迭代或类数组。实际开发中经常需要把它们转换成真正的数组。// NodeList - 数组 const divs document.querySelectorAll(div); const divArray [...divs]; // 现代浏览器推荐 // 或者 const divArray2 Array.from(divs); // 更通用 // arguments - 数组 function test() { const args [...arguments]; console.log(args); } // 类数组对象 - 数组 const arrayLike { 0: a, 1: b, length: 2 }; const realArray Array.from(arrayLike); console.log(realArray); // [a, b]Array.from是 ES6 专门用来处理这种转换的方法它还接受一个映射函数作为第二参数const arr Array.from({ length: 10 }, (_, i) i * i); console.log(arr); // [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]这个特性在生成连续数字序列时非常方便比Array.from(Array(n).keys())更直观。4. reduce不只是遍历更是递归的扁平化思维4.1 快速上手 reduce 的核心概念reduce的直译是“归并”它还常被称为“折叠”。它的核心思想是遍历数组的每一个元素把这个元素和上一次迭代的结果做某种运算最终归并成一个值。const arr [1, 2, 3, 4, 5]; const sum arr.reduce((acc, item) acc item, 0); console.log(sum); // 15参数说明回调接收四个参数accumulator累计值、currentValue当前值、currentIndex当前索引、array原数组。第二个参数是initialValue初始累计值强烈建议永远显式提供。不提供initialValue时数组的第一个元素会成为初始累计值回调从第二个元素开始执行。如果数组为空直接抛错TypeError: Reduce of empty array with no initial value。显式传入初始值可以消除这两个陷阱。4.2 用 reduce 实现 map 和 filter理解了reduce的归并逻辑后你会发现它能实现所有其他迭代方法。这不只是炫技而是帮助你真正理解reduce的威力。用 reduce 实现 mapconst arr [1, 2, 3]; const doubled arr.reduce((acc, item) { acc.push(item * 2); return acc; }, []); console.log(doubled); // [2, 4, 6]用 reduce 实现 filterconst arr [1, 2, 3, 4, 5]; const evens arr.reduce((acc, item) { if (item % 2 0) { acc.push(item); } return acc; }, []); console.log(evens); // [2, 4]reduce 实现数组扁平化const nested [[1, 2], [3, [4, 5]], 6]; function flatten(arr) { return arr.reduce((acc, item) { return acc.concat(Array.isArray(item) ? flatten(item) : item); }, []); } console.log(flatten(nested)); // [1, 2, 3, 4, 5, 6]4.3 工程中真正实用的 reduce 场景reduce 最有价值的场景是“把一个数组归并成某个非数组值”。场景一统计数组元素出现次数const fruits [apple, banana, apple, orange, banana, apple]; const count fruits.reduce((acc, fruit) { acc[fruit] (acc[fruit] || 0) 1; return acc; }, {}); console.log(count); // { apple: 3, banana: 2, orange: 1 }场景二数组去重const nums [1, 2, 2, 3, 4, 4, 5]; const unique nums.reduce((acc, item) { if (!acc.includes(item)) { acc.push(item); } return acc; }, []); console.log(unique); // [1, 2, 3, 4, 5]去重的更优写法是配合Set但Set去重本身也是一种遍历思想const unique [...new Set(nums)];场景三多个独立指标一次遍历完成const data [10, 20, 30, 40]; const result data.reduce((acc, item) { acc.sum item; acc.max Math.max(acc.max, item); acc.min Math.min(acc.min, item); return acc; }, { sum: 0, max: -Infinity, min: Infinity }); console.log(result); // { sum: 100, max: 40, min: 10 }如果对性能有极致追求这种一次遍历同时算多个指标的方式比分别调用多次reduce或多次循环更高效。场景四数组转对象const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 } ]; const userMap users.reduce((acc, user) { acc[user.id] user; return acc; }, {}); console.log(userMap); // { 1: { id: 1, name: 张三 }, 2: { id: 2, name: 李四 } }这种“数组转字典”的模式在业务中非常常见比如给select组件准备数据索引。4.4 手写 reduce真正理解它的运行机制写一个简化版的reduce可以帮助你彻底搞清楚它的执行流程function myReduce(arr, callback, initialValue) { let acc initialValue; let startIndex 0; // 处理未提供初始值的情况 if (arguments.length 3) { if (arr.length 0) { throw new TypeError(Reduce of empty array with no initial value); } acc arr[0]; startIndex 1; } for (let i startIndex; i arr.length; i) { acc callback(acc, arr[i], i, arr); } return acc; }这里要提醒的是这个简化版没有处理稀疏数组的空槽位问题。真正的原生reduce会跳过空槽位。如果要严谨复刻内部遍历时要判断i in arr。function myReduce(arr, callback, initialValue) { let acc initialValue; let startIndex 0; let hasInit arguments.length 3; for (let i 0; i arr.length; i) { // 跳过稀疏数组的空槽位 if (!(i in arr)) continue; if (!hasInit) { acc arr[i]; hasInit true; continue; } acc callback(acc, arr[i], i, arr); } if (!hasInit) { throw new TypeError(Reduce of empty array with no initial value); } return acc; }注意reduce的回调返回的累加值会被自动传给下一次迭代所以回调内部必须显式return更新后的acc。忘记返回值是使用reduce最常见的 bug这个 bug 的表象是“结果永远是初始值”。5. 性能、实战和踩坑指南5.1 冷门但有用的遍历方法速览除了上面的主流方法还有几个不常被提到但很有用的方法。Array.prototype.keys()/values()/entries()这三个方法返回迭代器对象分别产出索引、值、键值对const arr [a, b, c]; for (const key of arr.keys()) { console.log(key); // 0, 1, 2 } for (const value of arr.values()) { console.log(value); // a, b, c } for (const [key, value] of arr.entries()) { console.log(key, value); // [0, a], [1, b], [2, c] }Array.prototype.entries()特别适合在for...of里同时要索引和值的场景比forEach更灵活因为可以break。Array.prototype.flat()和flatMap()flat用于扁平化嵌套数组const nested [1, [2, [3, [4]]]]; console.log(nested.flat()); // [1, 2, [3, [4]]] 默认拍平一层 console.log(nested.flat(2)); // [1, 2, 3, [4]] console.log(nested.flat(Infinity)); // [1, 2, 3, 4] 完全拍平flatMap是先map再flat(1)的组合在同时进行映射和拍平时的场景非常方便const sentences [Hello world, JavaScript is awesome]; const words sentences.flatMap(sentence sentence.split( )); console.log(words); // [Hello, world, JavaScript, is, awesome]5.2 不同遍历场景的选型建议根据实际业务需求我整理了一张选型速查表场景推荐方案理由只要遍历执行副作用forEach语义清晰简洁需要提前退出for...ofbreak可中断可读性好数组映射生成新数组map语义明确链式调用友好筛选子集filter语义明确查找第一个满足条件的元素find语义明确不会多遍历判断是否存在满足条件的元素some短路特性性能好判断是否全部满足every短路特性性能好归并计算出一个值reduce唯一正解同时需要索引和值for...ofentries()可中断语义清晰对象属性遍历Object.keys()/Object.entries()for...of避免原型链属性超大数组 极致性能for循环函数调用开销最小5.3 实战中的典型报错和解决思路问题一forEach 配合 async/await 失效这是社区最经典的问题之一。forEach不会等待异步回调完成它只是把回调依次压入事件队列。正确做法是改用for...of// 错误的做法 async function processItems(items) { items.forEach(async (item) { await fetchData(item); // 不会按顺序执行 }); console.log(done); // 可能在 fetchData 之前就打印 } // 正确的做法 async function processItems(items) { for (const item of items) { await fetchData(item); // 逐个等待 } console.log(done); // fetchData 之后才打印 }问题二遍历过程中删除元素假如你要删除数组中所有值为 2 的元素用forEachsplice会出现“元素跳过”的 bugconst arr [1, 2, 2, 3, 4]; arr.forEach((item, index) { if (item 2) { arr.splice(index, 1); // 删除后后面的元素会向前移动索引错位 } }); console.log(arr); // [1, 2, 3, 4] —— 第二个 2 没被删除解决方案有三个方案一用filter生成新数组推荐最简单const arr2 arr.filter(item item ! 2);方案二倒序遍历for (let i arr.length - 1; i 0; i--) { if (arr[i] 2) { arr.splice(i, 1); } }方案三用while循环配合索引自减let i 0; while (i arr.length) { if (arr[i] 2) { arr.splice(i, 1); } else { i; } }问题三前端常见的 failed to load module script 报错现在很多项目都在用 ES Module 开发。如果你遇到类似报错Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of text/html这通常不是数组遍历的问题而是模块路径配置不对服务器返回了 HTML 页面。但要提一句这和遍历相关的场景通常出现在你使用动态import()遍历加载模块时const modules [a, b, c]; const loaded await Promise.all( modules.map(m import(./modules/${m}.js)) );如果路径写错了import()就会抛错。解决思路是检查模块文件是否存在、路径拼接是否正确、开发服务器的静态资源配置是否识别.js文件。问题四数组去重时用 includes 导致的性能问题在超大数组上去重如果用reduceincludes时间复杂度是 O(n²)数据的量级一旦上来就很吃力。更优的写法是利用Set去重时间复杂度降为 O(n)// 大数据量下不推荐的方式 const arr Array.from({ length: 100000 }, (_, i) i % 1000); const uniqueSlow arr.reduce((acc, item) { if (!acc.includes(item)) acc.push(item); return acc; }, []); // 更优的方式 const uniqueFast [...new Set(arr)];问题五稀疏数组的处理从 JSON 解析、CSV 导入等场景得到的数据可能包含空槽位或undefined遍历时容易出问题。稳妥的做法是先做数据清洗const raw [1, , 3, undefined, 5]; // 去除所有空槽位和 undefined const cleaned raw.filter(item item ! undefined); // 注意filter 会跳过空槽位所以上面的写法已经能处理 console.log(cleaned); // [1, 3, 5]如果你要保留undefined但要剔除空槽位可以用Array.fromconst normalized Array.from(raw); // 将空槽位转为 undefined console.log(normalized); // [1, undefined, 3, undefined, 5]5.4 遍历时修改数组的“红灯区”行为最后总结一下遍历中修改数组的关键规则这些都是我在实际代码走查里反复强调的高频问题操作是否安全说明遍历中追加元素不一定forEach不会遍历新元素for会遍历到新元素导致死循环遍历中删除元素有风险索引错位跳过元素遍历中替换元素有风险map/filter这类基于回调的遍历替换的是原数组元素但新数组的生成语义不变遍历中修改嵌套对象安全因为存的是引用修改的是对象内部属性遍历中重新给元素重新赋值安全但不推荐容易引起语义混乱注意如果你在for循环里arr.splice(index, 0, newItem)插入新元素循环索引会指向下一个元素可能把刚插入的元素刚好跳过也可能死循环。这类需求优先考虑生成新数组解决。结尾之前一点实在的建议我在团队代码评审里最常说的话是能用map就不要写forEachpush能用find就不要自己写for循环判断能用for...of就不要forEach里硬break因为根本 break 不了。代码写出来是给同事看的语义清晰比炫技重要得多。最后分享一个实用的小技巧如果你在 Chrome 控制台调试想要快速打印数组的索引和值可以用entries()const arr [a, b, c]; for (const [i, v] of arr.entries()) { console.log(i, v); // 0 a / 1 b / 2 c }这几个方法用熟了之后你写遍历会顺手很多也不容易写出那种跑起来才知道有问题的循环逻辑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价