ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

jstips 第 00 号技巧:向 JavaScript 数组中插入元素的高效之道——push、unshift、splice 的语义与 jsperf 性能实测

jstips 第 00 号技巧:向 JavaScript 数组中插入元素的高效之道——push、unshift、splice 的语义与 jsperf 性能实测 教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载向已有数组中插入元素是日常开发中再常见不过的操作push追加到尾部、unshift插入头部、splice插入中间。本篇以 jstips 项目第 00 号技巧见 _posts/zh_CN/javascript/2015-12-29-insert-item-inside-an-array.md为骨架完整继承原文在移动端与桌面端十余个浏览器环境下的实测性能数据并结合 JavaScript 数组的底层语义length属性、原地修改 vs 返回新数组、稀疏数组逐层剖析每种写法的取舍帮助你在不同场景下写出更快、更符合语义的插入代码。读完本文你将掌握尾部、头部、中间三种插入位置的全部可行写法、各自的副作用差异以及如何用基准测试验证更快的说法。为什么插入元素值得单独研究向数组插入元素看似简单但已知的方法并不代表没有更高效的选择。同样一个追加操作至少存在三种写法var arr [1, 2, 3, 4, 5]; var arr2 []; arr.push(6); // 方式一API 方法 arr[arr.length] 6; // 方式二直接索引赋值 arr2 arr.concat([6]); // 方式三拼接出新数组三种写法都能把6放进数组末尾但它们在是否修改原数组、返回值和执行速度上存在明显差异。jstips 原作者 loverajoel 通过 jsperf 基准测试给出了一个反直觉的结论在这些测试环境中arr[arr.length] 6这种绕开 API、直接索引赋值的写法常常比push更快而concat几乎总是垫底。本文后面会逐项展开这些数据。向数组尾部插入元素三种写法与原理原文给出了尾部插入的三种候选写法。先看它们的语义差别写法是否修改原数组返回值说明arr.push(6)修改新长度数值Array.prototype.push官方 APIarr[arr.length] 6修改被赋的值6依赖length恒为最大索引 1的语义arr2 arr.concat([6])不修改新数组返回拼接后的新数组原数组保持不变为什么arr[arr.length] 6等价于pushJavaScript 数组的length属性总是等于当前最大索引 1对非稀疏数组而言。因此arr.length恰好指向数组末尾的下一个空位arr[arr.length] 6就是在该空位写入新值写入后引擎会自动把length扩展为length 1。这本质上是push的内部实现路径之一也因此两种写法在部分引擎如当时的 Android 4.2.2上性能几乎完全相同。注意arr.concat([6])与前两者不同——它不触碰原数组而是返回一个新数组。原文称两种方法都是修改原始数组指的是push与索引赋值concat应归类为生成新数组方案适合需要保留原数组的不可变风格场景。移动端实测数据jsperf: push-item-inside-an-array以下数据来自原文档是作者在 2015–2016 年各移动端浏览器上的实测单位 ops/sec即每秒操作次数越高越快环境arr.push(6)arr[arr.length] 6arr.concat([6])Androidv4.2.2与索引赋值相同 // 3,319,694 ops/sec3,319,694 ops/sec比前两者慢 50.61%Chrome Mobilev33.0.0慢 66.74%6,125,975 ops/sec慢 87.63%Safari Mobilev9慢 40.19%7,452,898 ops/sec慢 49.78%移动端平均慢 35.64%5,632,856 ops/sec慢 62.67%桌面端实测数据环境arr.push(6)arr[arr.length] 6arr.concat([6])Chromev48.0.2564慢 61.94%21,602,722 ops/sec慢 87.45%Firefoxv4456,032,805 ops/sec慢 0.52%几乎持平慢 87.36%IEv11慢 39.61%67,197,046 ops/sec慢 93.41%Operav35.0.2066.68慢 71.60%30,775,071 ops/sec慢 83.70%Safariv9.0.342,670,978 ops/sec慢 0.80%几乎持平慢 76.07%桌面端平均慢 34.66%42,345,449 ops/sec慢 85.79%从两组数据可以提炼出三个稳定结论concat在几乎全部被测环境中都是最慢的——因为它要分配一块全新的数组并复制所有元素开销天然更高push与索引赋值互有胜负但差距微小Chrome、IE、Opera、Safari 上索引赋值更快Firefox 上push略快差距仅 0.5% 量级汇总平均之后索引赋值以微弱优势胜出移动端平均 5,632,856 ops/sec桌面端平均 42,345,449 ops/sec。需要强调的是这些数字反映的是当时各引擎V8、SpiderMonkey、Chakra 等的实现水平不应视为今天的普适结论——现代引擎对push的优化程度已大不相同。它们的价值在于揭示性能取决于引擎实现细节这一事实以及提供一套可复用的基准测试方法论。向数组头部插入元素unshift 与 concat 的对决头部插入的候选写法如下var arr [1, 2, 3, 4, 5]; arr.unshift(0); // 修改原数组在头部插入 0 [0].concat(arr); // 返回新数组 [0, 1, 2, 3, 4, 5]原数组不变这里有一个容易被忽略的细节原文特别点出unshift修改的是原始数组concat返回的是新数组。两者语义不同不能只按性能数字做选择——如果你的后续代码依赖原数组被就地更新就必须用unshift如果希望保持原数组不变才考虑[0].concat(arr)。移动端实测数据jsperf: unshift-item-inside-an-array环境[0].concat(arr)arr.unshift(0)Androidv4.2.21,808,717 ops/sec慢 97.85%Chrome Mobilev33.0.01,269,498 ops/sec慢 99.86%Safari Mobilev9慢 33.67%3,250,184 ops/sec移动端平均4,972,622 ops/sec慢 64.70%桌面端实测数据环境[0].concat(arr)arr.unshift(0)Chromev48.0.25642,656,685 ops/sec慢 96.77%Firefoxv448,039,759 ops/sec慢 99.72%IEv113,604,226 ops/sec慢 98.31%Operav35.0.2066.684,102,128 ops/sec慢 97.44%Safariv9.0.3慢 15.17%12,356,477 ops/sec桌面端平均6,032,573 ops/sec慢 78.65%与尾部插入相反头部插入的测试结果高度偏向concat在 Chrome、Firefox、IE、Opera、Android 上concat领先unshift多达 96%99%。原因不难理解unshift需要在原地把已有元素全部向后移位本质是 O(n) 的搬移操作而concat可以一次性分配并拷贝出新数组在当时的引擎实现中反而更快。Safari 是唯一例外unshift更快再次说明性能结论必须绑定具体引擎。向数组中间插入元素splice 是最优解中间位置插入只有一种主流写法原文的评价是简单且最高效var items [one, two, three, four]; items.splice(items.length / 2, 0, hello); // 结果[one, two, hello, three, four]splice 参数逐项拆解Array.prototype.splice(start, deleteCount, ...items)的三个关键参数在这里分别是items.length / 2起始插入位置。当length 4时4 / 2 2即在索引 2 处three之前插入当length为奇数如 5时5 / 2 2.5splice 会将小数起始索引按引擎的ToInteger规则取整向下取整为 2插入点会略微偏向左侧0deleteCount 0表示不删除任何已有元素这是纯插入的语义hello要插入的新元素可传入多个值实现批量插入items.splice(2, 0, a, b)。items.length / 2这个写法巧妙地利用了中间位置 长度的一半这一几何直觉让代码无需硬编码索引即可在任意长度数组的中间落点是这段示例最值得借鉴的地方。为什么说 splice 是中间插入的最高效方式从数据结构层面看中间插入无法回避把插入点之后的元素整体后移这一 O(n) 操作任何纯 JS 方案如slice拼接items.slice(0, mid).concat(hello, items.slice(mid))都要付出相同的移位代价还要额外承担多次数组分配与拷贝。而splice由引擎以原生实现直接完成移位与扩容省去了中间临时数组因此是中间插入场景下最直接、最经济的方案。原文作者在多个浏览器与操作系统上复测结论保持一致。三种位置、六种写法语义与副作用速查表目标位置修改原数组返回新数组常用写法性能要点当时实测尾部push、arr[arr.length] 6concat([6])、ES6[...arr, 6]尾部追加用push或索引赋值需保留原数组用concat索引赋值与push相当concat明显更慢头部unshift[0].concat(arr)、ES6[0, ...arr]原地修改用unshift不可变风格用concat多数被测引擎中concat大幅领先unshiftSafari 除外中间splice无高效替代slice拼接更慢splice(mid, 0, item)唯一正解引擎原生实现移位选择原则可以浓缩为一句话先按语义需求是否允许修改原数组筛选写法再在同语义候选之间谈性能。项目上下文这条技巧在 jstips 中的位置本文对应的原始文档是 jstips 项目README.md描述其为 Useful JavaScript tips旨在用不到两分钟的时间分享关于性能、约定、技巧的短小精悍的 JS 知识的第 00 号技巧位列 README.md 技巧列表的起始位置与后续 73 条技巧共同构成一个循序渐进的 JavaScript 技巧库。同仓库中与数组相关的技巧可以与本篇互相印证、串联阅读_posts/en/javascript/2016-02-06-deduplicate-an-array.md讲解数组去重同样区分原始值与对象引用两种语义与本篇的原地修改 vs 新数组互为补充_posts/en/javascript/2017-03-12-3-array-hacks.md介绍 ES6 展开运算符spread operator在数组上的妙用其空位迭代、展开传参、Set 去重三个技巧与插入场景直接相关见下节_posts/en/javascript/2016-05-12-make-easy-loop-on-array.md给数组附加循环游标方法展示了在数组对象上扩展行为的另一种思路。各语言版本的同名文档_posts/es_ES/...、_posts/zh_TW/...内容与中文版一致也印证了这条技巧的社区共识度。如果你希望为项目贡献新的技巧条目可以参考仓库的 POST_TEMPLATE.md 了解文章格式约定。ES6 时代的补充展开运算符让插入更简洁本篇发布于 ES6 普及之前如今同样场景有了更声明式的写法。受 _posts/en/javascript/2017-03-12-3-array-hacks.md 中展开运算符思路的启发现代代码可以这样写const arr [1, 2, 3, 4, 5]; const atEnd [...arr, 6]; // 尾部插入返回新数组 const atHead [0, ...arr]; // 头部插入返回新数组 const atMiddle [...arr.slice(0, 2), hello, ...arr.slice(2)]; // 中间插入语义上展开运算符版本与concat同属返回新数组、不修改原数组的不可变风格且写法更直观性能上展开运算符本质上仍是分配新数组 拷贝元素与concat处于同一量级并不具备超越push/unshift/splice原地写法的优势因此实际选型建议是需要原地修改且追求速度 → 尾部用push/索引赋值、头部用unshift、中间用splice追求不可变风格与代码可读性 → 用展开运算符或concat。如何自行验证一份可复用的基准测试思路原文在结尾鼓励读者自行测试这正是 jsperf 类基准测试的正确姿态——性能结论必须绑定具体引擎与版本。如果你希望在现代环境复测本文结论可以这样做将待比较的写法分别放入独立的函数例如function pushEnd(a) { a.push(6); }与function indexEnd(a) { a[a.length] 6; }用console.time/performance.now()循环执行数十万次对比总耗时在 Chrome、Firefox、Safari 以及移动端真机上分别跑一遍观察结论是否一致注意控制变量确保两种写法操作的是同等规模的数组避免 JIT 优化或 GC 干扰单次测量。小结向数组插入元素这件事远不止会写这么简单尾部push与arr[arr.length] 6性能相当concat因额外分配新数组而明显偏慢头部多数被测引擎中[0].concat(arr)大幅快于unshift但两者语义新数组 vs 原地修改必须先行区分中间splice(items.length / 2, 0, item)是唯一且最高效的正解方法论任何性能结论都应像原文那样注明浏览器版本与测试环境并在自己的目标平台上复测后再做最终决策。掌握了这些写法的语义边界与性能特征你在日常编码中就能根据是否允许修改原数组、插入位置、追求可读性还是极限速度快速选定最合适的方案。赞分享教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载相关推荐jstips 第 00 期向数组中插入元素——尾部、头部、中间三种场景的最优写法与性能实测jstips 第 00 期向数组中插入元素——尾部、头部、中间三种场景的最优写法与性能实测 在 JavaScript 日常开发中向已有数组中插入元素是出现频教程Modern JavaScript Tutorial 数组实战五连操作掌握 push/pop、shift/unshift 与中间元素替换Modern JavaScript Tutorial 数组实战五连操作掌握 push/pop、shift/unshift 与中间元素替换 本篇技术指南以 Mo文档/教程前端JavaScript 数组基础实战用 push、shift、unshift 与中间元素替换完成五步增删改查JavaScript 数组基础实战用 push、shift、unshift 与中间元素替换完成五步增删改查 本文以现代 JavaScript 教程zh.ja文档教程前端上一篇CANN ops-math 算子 aclnnCdistBackward 深度解析两段式接口、反向梯度公式与 AscendC 确定性实现下一篇如何免费解锁WeMod专业版功能终极教程与完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进