- 教程
【免费下载链接】jstips
This is about useful JS tips!
在 JavaScript 日常开发中,向已有数组中插入元素是出现频率极高的操作:向尾部追加用push,向头部插入用unshift,向中间写入用splice。但这三个"众所周知"的方法并非在每种场景下都是最高效的选择——通过下标直接赋值、借助concat生成新数组,往往能获得显著更优的性能。本文以 jstips 项目(一个收录短小实用 JavaScript 技巧的开源仓库)的第 00 期 tip Insert item inside an Array 为主体,完整复现其在移动端与桌面端多个浏览器上的基准测试数据,并结合数组方法的语义差异(原地修改 vs. 返回新数组),帮助你在真实项目中选出兼顾性能与副作用行为的插入方案。
一张表看懂:插入位置与候选 API
原文档开篇即给出结论:向数组尾部插入用push,头部用unshift,中间用splice。但进一步对比后,每种场景还有替代写法:
| 插入位置 | 原地修改原数组 | 返回新数组 |
|---|---|---|
| 尾部 | arr.push(item)/arr[arr.length] = item | arr.concat([item]) |
| 头部 | arr.unshift(item) | [item].concat(arr) |
| 中间 | arr.splice(index, 0, item) | 无(splice即原地操作) |
其中arr[arr.length] = item与[item].concat(arr)正是原文档重点推荐的"更高效的方式"。
向数组尾部添加元素:push、下标赋值与concat之争
var arr = [1,2,3,4,5]; var arr2 = []; arr.push(6); arr[arr.length] = 6; arr2 = arr.concat([6]);三种写法结果相同(数组都变成[1,2,3,4,5,6]),但语义有本质区别:
arr.push(6):原地修改原数组,返回新长度;arr[arr.length] = 6:由于length恒等于"当前最大下标 + 1",arr[arr.length]恰好指向末尾的下一个空位,效果等同于push,同样原地修改原数组;arr.concat([6]):不修改原数组,而是返回一个包含原数组全部元素与新增元素的新数组,因此必须用arr2接住返回值。
原文档特别提醒:前两种方法都会修改原始数组。如果调用方依赖原数组不被改动,concat才是唯一安全的写法——这也是"性能优先"之外必须考虑的功能约束。
移动端实测(ops/sec,越大越快)
原文档在 2015-2016 年代的移动端浏览器上测得以下数据:
| 浏览器 | arr.push(6) | arr[arr.length] = 6 | arr2 = arr.concat([6]) |
|---|---|---|---|
| Android (v4.2.2) | 3 319 694 | 3 319 694(与前等同) | 比其他两法慢 50.61% |
| Chrome Mobile (v33.0.0) | 慢 66.74% | 6 125 975(基准) | 慢 87.63% |
| Safari Mobile (v9) | 慢 40.19% | 7 452 898(基准) | 慢 49.78% |
移动端平均结论:arr[arr.length] = 6平均 5 632 856 ops/sec,arr.push(6)慢 35.64%,arr.concat([6])慢 62.67%。
桌面端实测
| 浏览器 | arr.push(6) | arr[arr.length] = 6 | arr2 = arr.concat([6]) |
|---|---|---|---|
| Chrome (v48.0.2564) | 慢 61.94% | 21 602 722(基准) | 慢 87.45% |
| Firefox (v44) | 56 032 805(基准) | 慢 0.52% | 慢 87.36% |
| IE (v11) | 慢 39.61% | 67 197 046(基准) | 慢 93.41% |
| Opera (v35.0.2066.68) | 慢 71.60% | 30 775 071(基准) | 慢 83.70% |
| Safari (v9.0.3) | 42 670 978(基准) | 慢 0.80% | 慢 76.07% |
桌面端平均结论:arr[arr.length] = 6平均 42 345 449 ops/sec,arr.push(6)慢 34.66%,arr.concat([6])慢 85.79%。
从这份数据可以读出两个稳定规律:在绝大多数被测浏览器中,arr[arr.length] = item与push性能相当甚至更快,而concat由于需要分配新数组并复制全部元素,几乎总是最慢的(慢 49%~93%)。差异最大的场景出现在 Firefox 与 Safari 上,push与下标赋值几乎打平(差距不足 1%)。原文档当时通过 jsPerf 基准测试工具测得上述结果,并提醒读者:不同引擎的优化策略不同,结论会随环境变化,建议在自己的目标环境里复测。
向数组头部添加元素:unshift与[0].concat(arr)的对决
var arr = [1,2,3,4,5]; arr.unshift(0); [0].concat(arr);原文档特别点出两者差异:unshift会编辑原始数组(原地修改,返回新长度);而[0].concat(arr)返回一个新数组,原数组保持不变。若要保留原数组用于后续逻辑,必须使用后者并接收返回值。
移动端实测
| 浏览器 | arr.unshift(0) | [0].concat(arr) |
|---|---|---|
| Android (v4.2.2) | 慢 97.85% | 1 808 717(基准) |
| Chrome Mobile (v33.0.0) | 慢 99.86% | 1 269 498(基准) |
| Safari Mobile (v9) | 3 250 184(基准) | 慢 33.67% |
移动端平均结论:[0].concat(arr)平均 4 972 622 ops/sec,arr.unshift(0)慢 64.70%。
桌面端实测
| 浏览器 | arr.unshift(0) | [0].concat(arr) |
|---|---|---|
| Chrome (v48.0.2564) | 慢 96.77% | 2 656 685(基准) |
| Firefox (v44) | 慢 99.72% | 8 039 759(基准) |
| IE (v11) | 慢 98.31% | 3 604 226(基准) |
| Opera (v35.0.2066.68) | 慢 97.44% | 4 102 128(基准) |
| Safari (v9.0.3) | 12 356 477(基准) | 慢 15.17% |
桌面端平均结论:[0].concat(arr)平均 6 032 573 ops/sec,arr.unshift(0)慢 78.65%。
头部插入的结果比尾部更悬殊:除了 Safari 外,[0].concat(arr)几乎全面碾压unshift,差距普遍高达 96%~99%。其原理并不难理解——unshift需要在数组头部腾出空位,必须把原有元素全部后移一位(相当于一次整体拷贝加移位);而concat在部分引擎中可以利用内存复制等底层优化直接拼接出新数组,避免了逐元素搬移的开销。当插入频率高、数组规模大时,头部插入优先选择[item].concat(arr)(并接受"返回新数组"的副作用),是在这些被测引擎上的普遍更优策略。
向数组中间添加元素:splice依然是最优解
var items = ['one', 'two', 'three', 'four']; items.splice(items.length / 2, 0, 'hello');Array.prototype.splice(start, deleteCount, ...items)的三个关键参数决定了它的行为:
start:开始修改的位置(索引)。负数表示从数组末尾倒数;大于数组长度时按数组长度处理;deleteCount:从start起要删除的元素个数。传0表示只插入、不删除——这正是"向中间插入元素"的标准用法;...items:要插入的元素,可以是多个,以逗号分隔依次插入。
上例中items.length / 2 === 2,因此元素'hello'被插入到索引 2 处('two'与'three'之间),结果为['one', 'two', 'hello', 'three', 'four']。splice是原地修改方法,会直接改写原数组。
原文档明确给出结论:在中间插入场景下,splice是最简单也是最高效的方式,作者在多款浏览器与操作系统上重复测试,结果均保持一致。相比用"拆成前后两段再重组"的写法,splice由引擎原生实现,避免了额外的数组分配与拼接开销。
实测数据的正确打开方式
需要特别说明的是,上文所有 ops/sec 数据均来自原文档在 2015-2016 年特定浏览器版本(Android v4.2.2、Chrome Mobile v33、Safari Mobile v9、Chrome v48、Firefox v44、IE v11、Opera v35、Safari v9.0.3)下通过 jsPerf 得到的测试结果。它们用于说明"不同写法之间存在数量级差异"这一结论非常直观,但不代表当今引擎的绝对性能——V8、SpiderMonkey 等引擎对push、unshift的优化策略早已多次迭代。正如原文档结尾所建议的:这些 tip 的价值在于启发你关注写法的性能差异,并鼓励你在自己的目标浏览器与操作系统上自行测试,用数据而不是直觉做决策。仓库中另一篇 tip Tip to measure performance of a JavaScript block 就专门介绍了用高精度时间戳测量 JS 代码块性能的方法,可作为自建基准的起点。
从 jstips 仓库延伸:数组操作的完整工具箱
本 tip 是 jstips 仓库(README.md)中编号为00的第一篇技巧。围绕"数组增删改查"这一主题,仓库里还收录了多篇可以串联阅读的姊妹篇:
- Two ways to empty an array:
list = []只是把变量指向新数组、旧引用仍持有数据;list.length = 0才会真正清空原数组内容并同步影响所有引用——与本篇"原地修改 vs. 新数组"的语义一脉相承; - Shuffle an Array:基于 Fisher-Yates 算法的原地洗牌,用到了本文
arr[i] = arr[j]式的下标直写; - 3 Array Hacks:稀疏数组的
map陷阱、用展开运算符传空参、[...new Set(arr)]去重; - Looping over arrays:
while、经典for、forEach、map、reduce、filter等遍历方案的完整梳理; - Array average and median:基于
reduce与排序求均值、中位数。
这些 tip 共同勾勒出数组操作的最佳实践图谱。如果你希望为仓库贡献新技巧,可以参考 CONTRIBUTING.md 的提交规范与 POST_TEMPLATE.md 的写作模板;本 tip 还提供了 西班牙语版 与 简体中文版 翻译,方便对照阅读。
小结:三种场景的最优选择
| 插入位置 | 首选写法 | 备选写法 | 注意事项 |
|---|---|---|---|
| 尾部 | arr[arr.length] = item | arr.push(item) | 两者均原地修改;arr.concat([item])返回新数组但最慢 |
| 头部 | [item].concat(arr) | arr.unshift(item) | 在多数被测引擎中concat明显更快,但返回新数组 |
| 中间 | arr.splice(index, 0, item) | — | deleteCount = 0表示只插入;原地修改原数组 |
最终的选择公式其实很简单:优先用下标赋值或push写尾部、用concat拼接写头部、用splice写中间;当副作用(是否修改原数组)比性能更重要时,则一律改用concat系列写法。至于极端场景下的最优解,记得回到自己的目标环境里跑一遍基准,让数据说话。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
在 JavaScript 中获取数组头部(head)与尾部(tail)元素:30-seconds-of-code 实战指南
在 JavaScript 中获取数组头部(head)与尾部(tail)元素:30 seconds of code 实战指南 数组的 头部(head) 指其第一个
教程文档30 seconds of code 实战:按条件从 JavaScript 数组头部或尾部提取元素
30 seconds of code 实战:按条件从 JavaScript 数组头部或尾部提取元素 本文基于 30 seconds of code 仓库中关于"
教程文档Webnovel Writer mainline_ready=false?如何快速用 preflight 和 doctor 四步定位
Webnovel Writer mainline_ready=false?如何快速用 preflight 和 doctor 四步定位 Webnovel Writ
人工智能AI 应用AI 写作RAGAI 插件AI 技能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考