JavaScript数组核心详解:从元素属性到遍历排序实战

发布时间:2026/9/28 8:40:22
JavaScript数组核心详解:从元素属性到遍历排序实战 1. 先从“元素”和“属性”这两个词说起很多人学JS数组时第一眼接触的就是“数组是一组有序数据的集合”然后背了一堆方法名写起来还是迷迷糊糊。我个人觉得真正理解数组的钥匙反而是先搞清楚标题里两个常被忽略的词元素和属性。数组里的“元素”就是你在方括号里放的每一项数据。它可以是数字、字符串、对象、函数甚至是一个嵌套的数组。数组里的“属性”最常见的是length它表示数组里有多少个元素。但这里的“属性”其实有两层含义一是数组自带的内建属性比如length还有通过原型链继承来的方法在语义上也算数组的能力二是数组元素本身也可以携带自定义属性因为数组是对象元素是对象的属性值这一点很多人没转过弯来。打个生活化的比方数组就像一栋公寓楼length是楼里登记的房间总数每个元素是住在某个房间里的“住户”。你不需要知道住户长什么样才能管理这栋楼只要知道房间号索引就能找到他。JS的数组索引从0开始所以arr[0]是第一个住户arr[length - 1]是最后一个住户。这套“编号房间”的模型只要你脑子里建立起来后面所有方法都不会再让你晕。那“属性”在JS里到底怎么体现你可以直接给数组挂自定义字段const arr [1, 2, 3]; arr.tag 我的数组; console.log(arr.tag); // 我的数组甚至给某个元素挂属性虽然实际开发里很少这么干但能帮助理解“数组即对象”arr[1].remark 这是第二个元素;因为数组本质上是特殊的对象数字索引会被转换为字符串键名存放在对象属性里。这就是为什么Object.keys([10, 20, 30])会得到[0, 1, 2]而不是[0, 1, 2]。这些细节写业务代码时未必用得上但一旦遇到“遍历数组时的键名判断”“把数组转成JSON后字段顺序是否符合预期”这类问题时理解就比死记硬背管用得多。2. 创建数组与初始化的多种姿势以及藏在里面的坑数组初始化几乎是每个JS项目里最常出现的操作但恰恰这里是新手踩坑最多的地方。先说推荐写法永远优先使用字面量。// 推荐 const list []; // 也推荐明确长度并填充 const matrix new Array(3).fill(0);字面量[]简单直观不会有歧义。而new Array()有个著名的坑当传入一个数字参数时它创建的不是“包含这个数字的数组”而是一个长度为该数字的空数组。const a new Array(3); console.log(a.length); // 3 console.log(a[0]); // undefined但不是真正的undefined是“空位”这种“稀疏数组”和[undefined, undefined, undefined]不完全等价。空位在forEach、map、filter等遍历方法中会被跳过而undefined作为明确的值会被正常遍历这个区别堪称经典“隐形Bug”。反过来如果你确实想创建“全0数组”最稳妥的是Array.fromconst arr Array.from({ length: 5 }, () 0); // [0, 0, 0, 0, 0]聊到初始化热词里还出现了“python创建数组元素为汉字”和“c字符串数组初始化”这正好引出一个跨语言对比。JS里创建包含中文的数组非常直接const names [张三, 李四, 王五];不需要指定长度也不需要声明类型因为JS数组是“动态数组”可以随时增删长度可变。这跟C那种“定长数组”或者Python中偏向“列表”的机制都不同。最直观的差异是C里你初始化int arr[5]就得负责管理这5个位置JS里你只管往里塞内存和扩容都交给引擎处理。理解这种动态性你才能明白为什么push就像往行李箱里硬塞一件大衣行李箱自己会变大而不是你在外面买箱子时就得定好容量。再补充一个很多教程不提的细节用fill初始化对象数组时填充的是同一个引用。const grids new Array(3).fill([]); grids[0].push(1); console.log(grids); // [[1], [1], [1]]三个子数组指向同一个对象修改一个其他全变。这也是热词里“二维数组初始化”最容易踩的坑。正确定义二维数组的方式是循环创建const rows 3; const cols 4; const matrix Array.from({ length: rows }, () new Array(cols).fill(0));每一行是独立的新数组互不干扰。这个细节你在面试里答出来基本能秒杀一大半只背了数组方法的候选人。3. 访问、修改、删除数组元素不只push和pop数组的增删改查是日常编码里跟“元素”打交道最频繁的场景。很多人一说删除就只会splice一说增加就只会push这当然能干活但理解不同操作的适用场景和性能差异才是经验分水岭。先看最常用的尾部操作const stack [1, 2, 3]; stack.push(4); // 尾部加一个 const last stack.pop(); // 尾部弹出一个这两兄弟的时间复杂度是O(1)也就是不管数组多大操作耗时基本恒定。而shift和unshift是头部操作每执行一次所有元素都得挪位置时间O(n)。数组越大代价越大。所以“用数组模拟队列”时如果数据量很大用shift会明显卡顿更优做法是维护一个头部索引或者直接改用链表思路。再说splice这是最灵活的增删工具一个方法能完成删除、替换、插入三种动作const arr [1, 2, 3, 4, 5]; // 删除从索引1开始删2个 arr.splice(1, 2); // [1, 4, 5] // 替换从索引0开始删1个换成9 arr.splice(0, 1, 9); // [9, 4, 5] // 插入从索引1开始删0个插入8 arr.splice(1, 0, 8); // [9, 8, 4, 5]splice的第二个参数是“删几个”而不是“删到哪个位置”这点是新手最常搞混的地方。网上不少教程会在评论区看到“为什么我删除数组指定元素删不掉”多半就是这里参数理解错了。删除指定元素到底怎么删如果是想按值删用filter更符合函数式思路const arr [1, 2, 3, 2, 4]; const withoutTwo arr.filter(item item ! 2);filter不会修改原数组而是返回新数组。这个特性在某些场景里是优点不污染原数据但也有缺点每次都复制一份大数组性能开销高。如果确定只删第一个匹配项可以直接用findIndex加splice组合const index arr.findIndex(item item 2); if (index -1) arr.splice(index, 1);我自己的建议是能不动原数组就尽量不动尤其是在React/Vue这类强调状态管理的框架里。直接改原数组容易引起“数据变得莫名其妙”的问题返回新数组让数据流更可预测。热词里“vue”出现多次关注Vue生态的读者一定要记住这一点arr[0] xxx这种按索引改值的方式在Vue2的响应式系统里并不会触发视图更新这就是数据监听层面的老坑。再来看查询。includes判断数组是否包含某个元素const arr [1, 2, 3]; console.log(arr.includes(2)); // true注意includes使用SameValueZero比较规则NaN也能正确匹配const nanArr [NaN]; console.log(nanArr.includes(NaN)); // true而indexOf会返回-1热词里“js判断字符串是否包含”也跟这里有交叉。字符串的包含判断用str.includes(subStr)数组的包含判断用arr.includes(item)两个方法同源不同物但都遵循“找到了返回true找不到返回false”的统一逻辑。你用一次就记住了。4. 数组排序sort方法里那个容易忽略的比较器数组排序的热度一直居高不下网上搜“js数组排序的几种方法”几乎每篇都是“冒泡排序、选择排序、插入排序、快速排序、sort方法”这种清单。但说句实在话业务开发里90%的排序需求Array.prototype.sort就够了你要会的是怎么正确使用它。眼看着简单一用就错的地方在于默认的sort先把元素转成字符串再按字典序排序。const nums [10, 9, 100, 2]; nums.sort(); console.log(nums); // [10, 100, 2, 9]因为字符串排序时把每个元素转成字符串“100”排在“10”后面“2”排在“9”后面完全不符合数值直觉。解决办法就是传比较器函数nums.sort((a, b) a - b); console.log(nums); // [2, 9, 10, 100]比较器返回负数、0、正数分别表示a应排前面、保持不变、a应排后面。a - b是升序b - a是降序。这个规则记牢比背十个排序算法都实用。再说对象数组按属性排序。比如你有一堆商品数据想按价格从低到高排const products [ { name: 键盘, price: 299 }, { name: 鼠标, price: 99 }, { name: 显示器, price: 1499 }, ]; products.sort((a, b) a.price - b.price);这个写法是sort最经典的业务场景。但有一点必须注意sort是原地排序它会修改原数组。如果你不想改动原始数据先拷贝一份再排const sorted [...products].sort((a, b) a.price - b.price);热词里还有“数组里面不能直接调用sort吗”这类搜索很多人疑惑为什么不行。其实不是不能调而是调用后原数组变了有时候你没意识到。比如先filter出一个子数组再sort你以为排序不影响原数组结果因为filter返回新数组才安全但如果直接对原数组sort原数组就被重排了。这种细微差别就是经验积累的地方。再补充一个排序场景“中文数组按拼音排序”。原生sort默认按Unicode码点排中文会得到“拼音不一定、码点一定”的结果。想要按拼音排可以用localeCompareconst names [张三, 李四, 王五]; names.sort((a, b) a.localeCompare(b, zh));localeCompare是字符串的方法给中文排序时指定语言标签会比默认排序更符合直觉。这个技巧在CMS后台按姓名排序、城市列表排序时特别好用。5. 数组遍历的几种常见姿势别再只会for循环遍历是数组操作里最“日常”的动作但要写出高效、可读性强的遍历是需要选择的。for循环、forEach、map、filter、reduce、some、every、for...of每个都有自己的适用场景。forEach适合“对每个元素做点事但不需要新数组”的情况arr.forEach(item { console.log(item); });map适合“把每个元素转换后收集成新数组”的情况const nums [1, 2, 3]; const doubled nums.map(n n * 2); // [2, 4, 6]filter适合“按条件保留元素”的情况这个前面已经讲过。reduce则适合“把一个数组累加成单个值”的情况const total [1, 2, 3, 4].reduce((sum, cur) sum cur, 0); // 10如果你只是想判断“数组里有没有满足条件的元素”用someconst hasEven [1, 2, 3].some(n n % 2 0); // true想判断“是不是所有元素都满足条件”用everyconst allPositive [1, 2, 3].every(n n 0); // true这几个方法一旦用熟你会发现很多原本要写五行的循环一行就能表达清楚代码的“意图”也一眼就能看出来。函数式编程的核心思想就是把“怎么做”交给底层实现你只需要声明“做什么”。热词里出现“js深入浅出vue”顺带提一句Vue的模板里遍历列表时用的v-for底层也依赖数组遍历和key管理。你给每项加的key本质上就是给数组元素的索引打上一个“稳定标识”方便框架最小化DOM操作。如果你在Vue里用index作为key一旦数组头部插入一个元素所有元素的下标都变了Vue会认为整个列表都变了从而引发不必要的重渲染。理解数组索引和元素之间的关系对前端框架调优非常关键。再说for...of这个循环可以遍历类数组对象和可迭代结构for (const item of arr) { console.log(item); }它和for...in有本质区别。for...in遍历的是键名字符串形式的下标而for...of遍历的是值。有人会在for...in里取到数组的自定义属性名比如给数组挂了arr.tag 标记for...in会把tag也遍历出来而for...of不会。所以遍历数组请优先用for...offor...in留给对象。6. 二维数组、扁平化与矩阵操作热词里反复出现“二维数组”、“矩阵元素的边界值”、“numpy三维数组相乘”这些其实是数组嵌套的高级话题。JS里二维数组的实质就是“数组套数组”比如const grid [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ];访问grid[1][2]就是先拿第二行[4, 5, 6]再拿它的第三个元素6。这在处理表格数据、棋盘类游戏、图像像素矩阵时特别常见。二维数组最大也最容易被忽视的坑就是“引用共享”前面提过的new Array(3).fill([])就是一种。除此之外还有一个“边界值”问题。比如一个3x3矩阵你想遍历所有元素判断“某元素是否在最外层边缘”通常的逻辑就是判断索引row 0 || row rows - 1 || col 0 || col cols - 1。这种边界处理要是漏了“边缘元素”的条件程序很容易越界或者漏算。凡是涉及矩阵遍历的算法题写代码前先把四周边界条件列出来比写好再调试快得多。再聊“扁平化”。把一个二维或三维数组拉平成一维在数据处理中很常用。JS新版提供了flat方法const arr [1, [2, [3, 4]]]; console.log(arr.flat()); // [1, 2, [3, 4]] 默认只拉平一层 console.log(arr.flat(2)); // [1, 2, 3, 4] 拉平两层 console.log(arr.flat(Infinity)); // [1, 2, 3, 4] 全部拉平flat不会修改原数组而是返回新数组。如果你在面试题里遇到手写扁平化思路就是递归检测每一项若该项还是数组就继续递归否则收集起来。这个写法跟flat内部实现逻辑基本一致。热词里还出现了“numpy三维数组相乘”这里存在语言的边界。NumPy本身跟JS没关系我的建议是如果你的项目里真的需要高性能的多维数组运算JS的普通嵌套数组只适合“业务逻辑层”不适合“科学计算层”。真要算矩阵乘法之类有两个正经思路一是用typedArray类型化数组加上自己写运算适合对性能有要求的场景二是直接用WASM或者WebGPU把计算丢给更底层的工具。普通网页应用如果偶尔做一点矩阵计算直接用嵌套数组加循环也够了没必要上重型工具。7. 数组与字符串、类数组的互转实战中的高频组合数组操作不是孤立的尤其是“字符串拆分转数组”和“类数组转数组”这两个场景几乎每个项目都会遇到。先说字符串转数组。最简单的方式是splitconst str a,b,c; console.log(str.split(,)); // [a, b, c]拷Excel表数据时经常要“按逗号分隔”或“按行分割”一行代码就能搞定const lines rawText.split(\n).map(line line.trim());热词里“excel 提取前两列匹配的数据成一个数组”就是这个场景的变体。你在浏览器里拿到CSV文本先按\n切出行再按,切出列然后用filter筛选需要的行最后落到数组里。这个链路比任何库都轻量也特别好调试。再说类数组转真数组。类数组长什么样最典型的是arguments对象函数内部可用、DOM查询得到的NodeList、字符串本身有索引也有length。它们有数字索引和length属性但不是真正的数组没有map、filter这些方法。标准的转换方式是const realArray Array.from(nodeList); // 或者老一点的写法 const realArray2 Array.prototype.slice.call(nodeList);热词里“playwright定位元素”“dom元素”都跟这个相关。你用document.querySelectorAll拿到NodeList后如果不转成真数组就不能直接调map很多刚接触DOM操作的人会在这里报错“xxx is not a function”。Array.from是现在最推荐的方案它既能转类数组又能接受映射函数const texts Array.from(document.querySelectorAll(.item), el el.textContent);一步到位把DOM元素数组转成文本数组。这个写法在处理页面数据采集时堪称神器。还有一个高频场景是“数组去重”搜热词时经常和数组方法混在一起。用Set是最省事的方式const arr [1, 2, 2, 3, 3, 4]; const unique [...new Set(arr)]; // [1, 2, 3, 4]Set的特点是元素唯一展开回数组就自动去重了。注意它去重用的是SameValueZero规则NaN也能正确去重这比双循环判断indexOf要可靠得多。8. 常见问题与排查技巧数组相关的报错和Bug在开发中出现的频率非常高。我整理一下自己踩过和帮别人排查过的典型问题做成一分钟速查表现象原因解决办法arr[0]是undefined但数组明明有长度稀疏数组空位而不是真正的元素用for...of或Array.from处理避免遍历方法跳过sort()结果和预期数值顺序不同默认按字符串字典序排序传(a, b) a - bmap返回的数组长度和原来一样但值没变忘记写returnmap里必须显式返回新值filter后原数组变了filter不会变原数组你多半改的是对象元素内部检查是否修改了对象成员的属性splice删除时删多了/删少了第二个参数是数量不是目标索引先findIndex找到位置再spliceincludes找不到NaN如果是用indexOf才会这样用includesnew Array(3).fill([])修改一个子数组全变fill填充的是同一个引用用Array.from循环创建NodeList没有map方法类数组不是真数组用Array.from转换数组按索引赋值在Vue里不更新视图Vue2响应式限制用splice或Vue.setVue3不用管arr.length 0后数据清空但引用还在这是清空数组的常用手段但可能被其他变量引用考虑重新赋值arr []再分享一个排查数组问题时比较实用的习惯在遍历方法里用console.log打印中间态定位哪一步数据不对。const result rawData .map(item item.value) .filter(v v 10) .sort((a, b) a - b);如果你发现最终结果不对就在.map后面加一行console.log看看进入filter之前的数据是否符合预期。数组链式调用的好处是每一步都有明确的输入输出哪里断了电一目了然。另一个经验是大数组操作尽量避免在循环里反复使用splice或shift。比如你要删除所有符合条件的元素如果用filter就干净利落arr arr.filter(item !shouldDelete(item));如果必须在原数组上操作那就倒序遍历for (let i arr.length - 1; i 0; i--) { if (shouldDelete(arr[i])) { arr.splice(i, 1); } }倒序删除的妙处在于删除后面的元素不会影响前面元素的下标。很多人在正序遍历时删除当前项结果跳过了一个元素这种“看似没删干净”的Bug非常隐蔽。9. 最后分享一点个人总结数组这个东西说简单也简单无非是存数据、取数据、改数据说深也深length属性与索引的关系、稀疏数组的遍历差异、引用共享带来的副作用、遍历方法的性能差异每一条都能延展出一片知识海洋。我自己的体会是不要死记方法清单而是给每个方法找一个你在业务里真正会用到的场景。你做过“根据URL参数生成数组”“把接口返回的对象数组按条件分组”“把Excel粘贴文本拆成表格数据”再回头看split、filter、reduce这些东西就像工具箱里每一把用过的手钳闭着眼睛都知道该拿哪一把。最后再提供一个额外的编码习惯写数组相关代码时第一反应先想清楚“我要不要保留原数组”。如果要保留就先[...arr]或者arr.slice()拷贝一份再操作。这个习惯特别能降低出差错概率也让我在代码评审时少挨很多骂。如果你正在学JS试着把今天提到的场景挨个跑一遍用console.log看每一步的输出亲自感受一下sort某个数字数组的“诡异顺序”和fill共享引用的“连带修改”这比读十篇博客都管用。数组这门课动手跑一遍永远比背一遍记得牢。