ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript数组实战指南:从基础方法到性能优化与面试技巧

JavaScript数组实战指南:从基础方法到性能优化与面试技巧 开头做前端这几年我不敢说把 JavaScript 精通到什么程度但有一点体会特别深数组操作水平基本决定了你写业务代码的速度和质量。后端接口一返回前端拿到的基本都是数组表单提交前要组装数据也是数组状态管理里要筛选、去重、排序、分组还是数组。面试题里十道至少有五道跟数组有关工作中更是每天都在跟它打交道。这篇文章我打算把自己这些年在前端实战里用数组的整套心得整理出来。不是把 MDN 文档抄一遍而是结合真实业务场景讲讲哪些方法最常用、哪些写法有坑、什么数据结构在什么场合更快以及面试里考到的数组题该怎么思考和回答。无论你是刚入行的新人还是写了几年业务想系统巩固一下这篇内容都可以当一份“带注释的实战笔记”来用。1. 创建、初始化与复制先把地基打牢1.1 字面量、构造函数和静态方法到底用哪个数组创建的写法很基础但我见过不少团队代码里混着各种风格出问题的还不少。最推荐的一定是字面量const list []; const nums [1, 2, 3];简单、直观、没有歧义。构造函数new Array()看起来也能用但有一个非常容易踩的坑new Array(5)并不是创建一个包含数字 5 的数组而是创建一个长度为 5 的空数组。这个行为容易让人懵尤其是从其他语言转过来的同学。你如果真想初始化一个“长度为 5 但每个元素都是 0”的数组得写const arr new Array(5).fill(0); // 结果是 [0, 0, 0, 0, 0]还有两个静态方法在实战里很常用Array.from和Array.of。Array.from最大的价值是把类数组对象转成真数组。比如 DOM 操作里经常拿到NodeList它不是数组不能直接用map、filter以前都是写Array.prototype.slice.call(nodeList)现在直接Array.from(nodeList)就行了。它还能接收一个映射函数const arr Array.from({ length: 5 }, (_, i) i * 2); // 结果是 [0, 2, 4, 6, 8]这个用法在生成测试数据、初始化表格行时非常有用。Array.of则是为了解决new Array(5)的坑而存在的Array.of(5)返回[5]而不是空数组。业务里它出现频率不高但一旦写工具函数需要“把参数变成数组”时它比Array.from(arguments)更直接。1.2 浅拷贝和深拷贝别再在这个环节丢数据数组拷贝是业务里最容易出问题的点。先说浅拷贝JavaScript 里常见的三种方式const copy1 original.slice(); const copy2 [...original]; const copy3 Array.from(original);这三种都是浅拷贝对于一维数组、元素为基本类型的场景完全够用复制后修改新数组不会影响原数组。但注意这里说的是“不会影响原数组”指的是最外层。如果数组里面放的是对象浅拷贝只是把对象的引用复制了一份const original [{ name: 张三 }, { name: 李四 }]; const copy [...original]; copy[0].name 王五; console.log(original[0].name); // 王五原数组也被改了。这种情况在列表编辑功能里非常常见弹窗里改了表单数据保存时发现列表中的原数据也被改了排查半天问题就出在浅拷贝上。真正的深拷贝在业务代码里如果对象结构不复杂可以用JSON.parse(JSON.stringify(arr))但它有两个明显的局限会丢失undefined、函数、Symbol类型的属性而且遇到循环引用会直接报错。所以更稳妥的方式是用structuredClone这是现代浏览器原生支持的深拷贝 APIconst deepCopy structuredClone(original);如果项目里有 Lodash_.cloneDeep也是常规选择。我的建议是简单结构用展开运算符浅拷贝复杂结构直接用structuredClone或cloneDeep不要为了省事把浅拷贝当深拷贝用。2. 实战中最高频的五类数组操作一次讲透2.1 map、filter、forEach 不要混用这三个方法是我看代码时最常看到用错的。“遍历数组修改元素”用forEach还是map“筛选出满足条件的元素”用for循环还是filter记住一个判断标准你要不要拿到一个新数组要基于原数组生成一个个数相同、内容变换后的新数组用map。要挑出部分元素组成新数组用filter。只是想遍历一遍、执行副作用比如打印、写入某个变量用forEach。业务里最常见的错误是在forEach里收集结果const result []; const list [1, 2, 3, 4]; list.forEach((item) { if (item % 2 0) { result.push(item); } });这样写功能没问题但不够“声明式”。同样的需求用filter一行搞定const result list.filter((item) item % 2 0);另一个更隐蔽的坑是在map或forEach里修改原数组元素尤其是forEach中直接对对象元素做变更。如果你确实要修改原数组直接用for...of反而更清晰。2.2 reduce 不只是用来求和很多前端同学一看到reduce就发怵觉得它的回调参数太多、返回值不好理解。实际上它的核心思想很简单把一个数组“归并”成一个结果。这个结果可以是数字、字符串、对象甚至一个新数组。最常见的求和const total orders.reduce((sum, order) sum order.amount, 0);第二个参数0是初始值。如果不传初始值第一次迭代时sum会取数组第一个元素这在数组为空时会直接报错所以使用reduce永远要传初始值。业务里reduce更强大的场景是把数组转成对象也就是“按某个字段做索引”const userMap userList.reduce((map, user) { map[user.id] user; return map; }, {});这样后面就不用每次find遍历整个数组直接userMap[id]取对象就行速度是O(1)。在做表格行操作、下拉框选项联动时这个模式能省很多代码。2.3 排序sort()不传比较函数坑你没商量sort()是 JS 里最容易翻车的数组方法之一。原因在于不传参数时它会先把元素转成字符串再按 Unicode 编码排序。所以数字排序会出现这样的结果const nums [10, 9, 80, 3]; nums.sort(); // 结果是 [10, 3, 80, 9]而不是 [3, 9, 10, 80]数字排序一定要传比较函数nums.sort((a, b) a - b); // 升序 nums.sort((a, b) b - a); // 降序对象数组按某个字段排序也很常见list.sort((a, b) a.price - b.price);多条件排序也不难只要比较函数里做“优先级”判断就行。比如先按销量降序销量相同再按价格升序list.sort((a, b) { if (a.sales ! b.sales) return b.sales - a.sales; return a.price - b.price; });如果比较字段是字符串需要用localeCompare特别是中文排序list.sort((a, b) a.name.localeCompare(b.name, zh-Hans-CN));2.4 数组去重不同场景用不同方案数组去重是前端面试题里的“常青树”业务里也经常遇到。最简单的是用Setconst unique [...new Set([1, 2, 2, 3, 3, 4])]; // 结果是 [1, 2, 3, 4]注意Set对 NaN 的处理和不太一样。Set内部使用SameValueZero算法会把 NaN 视为相等所以[NaN, NaN]用Set去重后只剩一个NaN这反而是我们期望的行为。真正麻烦的是对象数组去重。如果只是对象引用完全相同Set也能用但业务里通常是要按某个字段去重。比如后端接口返回了一批数据里面有重复的id我们要保留第一次出现的记录这时候用Mapconst seen new Map(); const uniqueList list.filter((item) { if (seen.has(item.id)) return false; seen.set(item.id, true); return true; });如果要按多个字段判断“同一条记录”可以把字段组合成 keyconst seen new Map(); const uniqueList list.filter((item) { const key ${item.id}-${item.type}; if (seen.has(key)) return false; seen.set(key, true); return true; });去重思路要灵活先搞清楚“重复”的定义是什么再选合适的数据结构。3. 真实业务场景从接口数据到页面渲染的常见链路3.1 场景一按条件筛选数组并展示一个非常典型的业务场景是搜索筛选。比如页面上有一个输入框用户输入关键字前端从接口返回的完整列表里实时过滤出匹配项。假设数据是这样一个结构const vehicleList [ { plateNumber: 京A12345, owner: 张三, type: 小型车 }, { plateNumber: 沪B67890, owner: 李四, type: 大型车 }, // ... ];输入车牌关键字时需要筛选出plateNumber中包含该字符的数据。这里的核心方法是filterincludesconst keyword 京A; const filtered vehicleList.filter((item) item.plateNumber.includes(keyword));很多同学会觉得这很简单但实际业务里会延伸出几个细节大小写问题。车牌里字母大小写不敏感includes是区分大小写的所以更稳妥的写法是统一转成大写或小写再比较const normalizedKeyword keyword.trim().toUpperCase(); const filtered vehicleList.filter((item) item.plateNumber.toUpperCase().includes(normalizedKeyword) );多字段搜索。有时候用户输入的可能是车主姓名的一部分也可能是车牌的一部分。这时候可以用一个“组合关键字”的方式把所有可搜索字段拼起来const filtered vehicleList.filter((item) [item.plateNumber, item.owner, item.type].some((field) field.toUpperCase().includes(normalizedKeyword) ) );我把这个逻辑封装成了一个工具函数项目中所有类似的搜索过滤都复用它function filterByKeyword(list, keyword, fields) { const normalized keyword.trim().toLowerCase(); if (!normalized) return list; return list.filter((item) fields.some((field) String(item[field]).toLowerCase().includes(normalized)) ); }3.2 场景二数组分割、截取与显示包含某一字符的元素另外一个常见需求是“把数组分割成几段”或者“从数组里提取所有包含某个字符的元素”。比如聊天记录里我们需要把所有包含某个标签的消息挑出来const messages [订单已发货, 退款申请中, 客服已回复, 订单已完成]; const orderRelated messages.filter((msg) msg.includes(订单));再比如分页效果前端一次性拿到全部数据需要按每页 10 条分割function paginate(list, pageSize 10) { const result []; for (let i 0; i list.length; i pageSize) { result.push(list.slice(i, i pageSize)); } return result; }此时slice的区间是[, )也就是包含起始位置、不包含结束位置。很多人记混了slice和spliceslice不会修改原数组splice会修改原数组。业务里大多数时候用slice就够了。如果你需要按某个字符分割字符串变成数组那是split的活注意别和splice混了。3.3 场景三数组与算法——树状数组、前缀和在前端的用武之地看到“树状数组”这个热词很多做业务的前端可能会觉得这是算法竞赛才用的东西。但我可以负责任地说如果你的项目里涉及大量区间求和、频繁更新某一位数据用普通数组每次遍历求和的代价是 O(n)数据量一大页面就会卡。比如后端返回了一份连续多天的报表数据前端需要频繁展示某一段日期的累计值这时候树状数组就能派上用场。树状数组的核心是维护一个tree数组支持两个操作add(index, delta)把第 index 个位置增加 deltasum(index)求前 index 个元素的和以长度为 16 的序列为例查询sum(11)时不是从 1 加到 11而是通过二进制拆分成若干段统计复杂度降为 O(log n)单点修改add(3, x)也只需要更新包含第 3 个位置的树节点同样 O(log n)。class FenwickTree { constructor(n) { this.tree new Array(n 1).fill(0); } add(index, delta) { while (index this.tree.length - 1) { this.tree[index] delta; index index -index; } } sum(index) { let result 0; while (index 0) { result this.tree[index]; index - index -index; } return result; } }实际项目中如果数据量只有几百上千条老老实实用reduce就行没有必要为了炫技引入树状数组。但如果你在处理大型表格、实时统计仪表盘这类高频更新的场景这个结构值得放进工具箱。3.4 场景四动态数组与海量数据渲染JavaScript 的数组本身就是动态的push、pop在数组末尾操作性能最好unshift、shift在头部操作会让所有元素索引移动性能较差。在数据量大的循环里尽量不要在数组头部插入数据要保证顺序的话可以先push完再reverse。还有一个容易被忽略的点前端拿到大量数据时在主线程里做复杂的数组遍历和渲染页面会掉帧甚至白屏。这时候可以用Web Worker把数据处理任务扔到后台线程。比如上传大文件时前端要把文件切片然后对每个切片做 hash 计算再推进上传队列。这个过程如果全在主线程做用户会明显感觉到页面“卡住了”。把切片和 hash 计算的逻辑放进 Worker 后主线程只负责接收结果和更新 UIconst worker new Worker(new URL(./upload.worker.js, import.meta.url)); worker.postMessage({ file, chunkSize: 1024 * 1024 }); worker.onmessage (e) { // 拿到分片数组开始逐片上传 };数组本身不是性能瓶颈瓶颈在于你在什么线程、用什么复杂度在操作它。这句话记住了处理大数据量时能少踩很多坑。4. 前端数组的引用陷阱与跨端传参4.1 组件间传参数组被误改的教训Vue 和 React 项目里都有一个经典问题父组件把一个数组传给子组件子组件内部做了修改结果父组件的数据也变了。根本原因是数组是引用类型子组件拿到的是同一个引用。比如 React 中// 子组件内部 props.list.push(newItem); // 直接改原数组在 React 18 严格模式下的开发环境这个问题尤其难排查因为push不会触发视图更新但数据已经悄悄被改了。正确的做法永远是先拷贝再修改const newList [...props.list, newItem];Vue 里面 “数组响应式” 的坑也属于这一类直接通过索引修改数组元素this.list[0] xxx在某些版本里不会触发视图更新所以 Vue 提供了Vue.set或者要求用splice方法。现在 Vue 3 用 Proxy 重写了响应式但很多旧代码里还留着这些写法理解原理才能兼容各种老项目。4.2 数组转字符串常用姿势和深坑数组转字符串看似简单业务场景却非常多接口需要逗号分隔的 id 列表、把数组作为 URL 参数传递、存储到localStorage里。三种常见方式const arr [1, 2, 3]; arr.toString(); // 1,2,3 arr.join(,); // 1,2,3 JSON.stringify(arr); // [1,2,3]toString本质上是join(,)的别名。如果你的元素里本身含有逗号比如[a,b, c]toString()会得到a,b,c看起来没问题但解析回去就乱了。这种场景建议用JSON.stringify或者约定一个不常用的分隔符。从字符串转回数组时split是最常用的。但如果你是用JSON.stringify存进localStorage取出来时要记得JSON.parse。有一回我在存一组 id 时用了toString()数据变成了1,2,3取出后用split(,)转回来本来没问题。结果产品改需求id 变成了类似A-1这样的字符串整个解析就崩了。后来我就统一了凡是跨接口、跨存储的数组传递一律用 JSON 序列化格式清晰不怕特殊字符。4.3 类数组、二维数组与“看起来像数组但不是数组”的家伙前端实际执行环境里有一种非常常见的“类数组”arguments、NodeList、字符串。它们的共性是有length属性可以通过下标访问但没有数组的方法。所以想对它们用map、filter得先转成真数组const args Array.from(arguments); const nodes Array.from(document.querySelectorAll(.item));不转的后果是直接在 NodeList 上调用forEach在某些旧环境里也能跑但map就不行报错信息还很难看懂。二维数组在前端里的场景多为矩阵或表格数据的扁平化处理。热词里的 C 多维数组和指针在 JavaScript 里对应的是数组的数组。比如一个 3 行 4 列的表格const matrix [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], ];要遍历所有元素可以双重循环也可以用flat()把二维数组变成一维数组。默认flat()只展开一层多层嵌套要传深度flat(Infinity)。flatMap是map后再flat(1)的合成操作比如把一组订单里的商品明细拉平const orderProductList orders.flatMap((order) order.products);这类“看起来绕一下”的写法在数据聚合时能省不少中间变量。5. 面试题视角2026 年数组考点到底在考什么5.1 手写实现考的是对底层细节的理解这两年前端面试题的趋势很明显直接问 API 怎么用的少了让人手写实现核心方法、或者“基于某个 API 说原理”的多了。数组这一块的高频手写题有手写reduce手写flat/ 扁平化数组手写深拷贝手写去重到“既能去重又能统计次数”手写数组乱序洗牌以reduce为例手写实现的关键点是回调参数顺序、初始值缺省时的行为、空数组报错逻辑function myReduce(arr, callback, initialValue) { let accumulator initialValue; let startIndex 0; if (initialValue undefined) { if (arr.length 0) { throw new TypeError(Reduce of empty array with no initial value); } accumulator arr[0]; startIndex 1; } for (let i startIndex; i arr.length; i) { accumulator callback(accumulator, arr[i], i, arr); } return accumulator; }写清楚这些细节说明你不仅用过reduce还理解它的实现思路面试评价会好很多。5.2 经典算法题三个数组最大乘积、两数之和与背包思想热词里有一条“三个数组最大的乘积”这个题目看起来是数学题其实考察的是分类讨论和数组遍历。给定一个数组找三个数乘积最大。常规思路是先排序比较两个最大正数和最大负数的情况function maxProduct(nums) { nums.sort((a, b) a - b); const n nums.length; return Math.max( nums[n - 1] * nums[n - 2] * nums[n - 3], nums[0] * nums[1] * nums[n - 1] ); }为什么第二种情况是nums[0] * nums[1] * nums[n - 1]因为如果数组里有两个很大的负数它们相乘会得到正数再乘上最大的正数可能比三个正数的乘积还大。这类题目面试官不是真的想让你背答案而是想看你能不能把边界情况列全。热词里还有一条“已知固定数值确定数组中的哪些数据和等于固定值”。这其实是一个子集求和问题。如果数据量小可以用回溯枚举所有组合如果数据量大就是动态规划或背包思想。前端面试里通常不会要求最优解但要求你至少能写出递归 剪枝的版本并且说清楚时间复杂度。这些题刷一遍的意义不在于真的在工作中如此用而在于锻炼“把业务需求抽象成数组问题”的能力。我这几年写业务最复杂的场景往往就是把一个模糊的产品需求翻译成数组变换。5.3 会写代码也要会表达技术选型阶段怎么讲面试题后面其实还有一层隐藏考察你能不能讲清楚“为什么选这个方案”。比如面试官问“要对一个数组去重你会怎么做”如果你能这样答面试官体感会完全不同先用Set做基本类型去重因为写法最简洁、性能也够如果数组很大、类型复杂再考虑用Map按字段去重如果去重的判断条件是深层次属性组合就需要写一个通用的uniqueBy(list, keyFn)工具函数。同时补充一句去重前要明确“重复”的定义是按引用、按内容还是按某个字段这个前提比去重方法本身更重要。这种回答方式在工作中同样适用。写代码不是把功能做出来就完了你总得面对 code review也得面对同事问你“这段逻辑什么意思”。能把选择理由讲清楚说明你真的理解它。结束语个人体会我把这些数组操作的思路整理成了一个小工具函数库里面有uniqBy、filterByKeyword、paginate、sumBy、groupBy这些每天都在用的函数。回头翻看时发现绝大部分复杂业务逻辑拆到最后就是几个数组方法的组合。所以我对新人的建议从来都是别急着上框架先把数组、对象这些基础数据结构玩透。你在业务里看到的所有“高级技巧”底层都是这些基础能力的组合。最后再分享一个小技巧调试数组操作时别只盯着最终结果多用console.log把每次map、filter、reduce之后的中间状态打出来看。很多看起来很玄乎的问题其实只是中间某一步产生了一个空数组或undefined。把每一步都想清楚数组这个东西真没那么可怕。
返回列表