組添加元素6種方法:從push到展開運(yùn)算符的性能與選型)
做前端這些年被問得最多的一類問題就是往數(shù)組里添加元素有哪幾種寫法很多人脫口而出push想一下再補(bǔ)一個(gè)unshift能說出六種以上的其實(shí)不多。更別提問一句“為什么unshift慢慢多少什么場(chǎng)景該選哪種”——能講清楚的人更少。這篇文章就把 JS 數(shù)組添加元素的 6 種方法挨個(gè)拆開從語法、返回值、底層存儲(chǔ)到性能實(shí)測(cè)再結(jié)合我自己的項(xiàng)目經(jīng)驗(yàn)給出選型建議。內(nèi)容適合初中級(jí)前端、準(zhǔn)備面試的同學(xué)也適合正在做性能優(yōu)化又不想盲目換數(shù)據(jù)結(jié)構(gòu)的同行。1. 先分清兩類操作原地修改與原數(shù)組不變1.1 為什么一定要先搞清楚“改沒改原數(shù)組”數(shù)組添加元素的方法看似零散其實(shí)可以一刀切成兩類一類會(huì)直接修改原數(shù)組另一類不會(huì)。先列個(gè)清單原地修改push、unshift、splice、arr.length ...、arr[index] ...返回新數(shù)組concat、展開運(yùn)算符[...]這個(gè)分類不是面試炫技用的它直接決定了你在什么場(chǎng)景下敢用哪個(gè)方法。舉個(gè)例子在 React 的 state 里管理列表如果你直接push就算數(shù)據(jù)變了組件也可能不更新因?yàn)?state 引用沒變?cè)诤瘮?shù)式編程里副作用會(huì)引起各種隱蔽 bug。反過來在高頻操作的內(nèi)部邏輯里如果每加一個(gè)元素就復(fù)制一次整個(gè)數(shù)組內(nèi)存和 GC 開銷也會(huì)讓你難受。所以每次寫“加元素”之前先回答自己這個(gè)數(shù)組能不能被修改1.2 一個(gè)類比的快速理解把數(shù)組想象成一份貼在墻上的名單。push就是在名單末尾追加一個(gè)新名字原名單還在返回值是你重新數(shù)了一遍的總?cè)藬?shù)。unshift更像插隊(duì)你要把所有人的名字往后挪一格再把新名字貼到最前面。concat和展開運(yùn)算符就是完全不碰原來那份名單重新抄一份完整的名單新名字按你的要求出現(xiàn)在某個(gè)位置原來的名單保存不動(dòng)。抄名單這種操作在人數(shù)少的時(shí)候很輕松人數(shù)一多每次都要把整個(gè)名單謄一遍自然就慢。這個(gè)類比能解釋大部分性能差異原地操作用的是同一份名單不可變操作每次都造一份新名單頭插和中間插入需要挪動(dòng)名單上的人名挪動(dòng)數(shù)量越多越慢。接下來逐個(gè)看具體方法。1.3 六種方法的基本行為一覽方法語法示例是否改原數(shù)組返回值插入位置pusharr.push(item)是新 length尾部unshiftarr.unshift(item)是新 length頭部splicearr.splice(i, 0, item)是被刪除元素?cái)?shù)組任意位置concatarr.concat(item)否新數(shù)組尾部展開運(yùn)算符[...arr, item]否新數(shù)組尾部或任意位置length/索引賦值arr[arr.length] item是元素值尾部這張表先給你一個(gè)整體印象原地修改的方法里能避免搬移元素的操作必然更快返回新數(shù)組的方法必然有一次完整復(fù)制。記住這個(gè)基本判斷再看后面性能和引擎原理的部分就不會(huì)亂。2. 六個(gè)方法逐個(gè)拆解語法、行為、隱藏細(xì)節(jié)2.1push尾部追加push是數(shù)組添加元素最常用的方法。它的語法是arr.push(element1, element2, ..., elementN)可以一次傳入多個(gè)參數(shù)所有參數(shù)會(huì)按順序追加到數(shù)組末尾。它的返回值很有特點(diǎn)是操作完成后數(shù)組的新長(zhǎng)度而不是被添加的元素。這一點(diǎn)經(jīng)常有人記錯(cuò)。push的性能優(yōu)勢(shì)來自它的操作路徑特別短引擎只需要知道當(dāng)前數(shù)組長(zhǎng)度是多少然后在那個(gè)下標(biāo)位置寫入新值再把長(zhǎng)度加一。如果數(shù)組預(yù)留的空間不夠了就觸發(fā)一次擴(kuò)容把容量擴(kuò)大。擴(kuò)容是成倍擴(kuò)大所以均攤到每次添加也是常數(shù)時(shí)間。這也是為什么尾部插入在絕大多數(shù)情況下都是最快的添加方式。需要提醒的是push是原地修改方法它會(huì)把原數(shù)組改掉。如果函數(shù)內(nèi)部對(duì)入?yún)?shù)組做了push外層拿到的數(shù)組也會(huì)變。想避免這個(gè)副作用就得用后面的concat或展開運(yùn)算符。2.2unshift頭部插入unshift的語法和push幾乎對(duì)稱arr.unshift(element1, ..., elementN)直接在數(shù)組最前面插入一個(gè)或多個(gè)元素返回值同樣是新數(shù)組的length。它也是原地修改。但等價(jià)的語法不意味著等價(jià)的性能。頭部插入意味著原本位于 0 號(hào)索引的元素要變成 1 號(hào)索引1 號(hào)變 2 號(hào)所有已有元素都要往后挪一位。數(shù)組本質(zhì)上是連續(xù)內(nèi)存的線性結(jié)構(gòu)這個(gè)搬移操作是 O(n)n 是數(shù)組長(zhǎng)度。數(shù)組越長(zhǎng)unshift越慢。這里藏著一個(gè)容易踩的坑當(dāng)一次unshift傳入多個(gè)元素時(shí)參數(shù)順序沒有被反轉(zhuǎn)。比如arr [1, 2]執(zhí)行arr.unshift(3, 4)之后結(jié)果是[3, 4, 1, 2]不是[4, 3, 1, 2]。因?yàn)橐娴倪壿嬍前褏?shù)按順序放到數(shù)組頭部的連續(xù)區(qū)域里。很多人下意識(shí)以為會(huì)像“從前往后插”那樣反過來實(shí)際不是。2.3splice任意位置插入splice是數(shù)組里最“多功能”的方法刪除、替換、插入一套全包。往數(shù)組添加元素的用法是arr.splice(start, 0, item1, item2, ...)第二個(gè)參數(shù)傳 0 表示不刪除任何元素后面的參數(shù)就是要插入的元素。它返回一個(gè)新數(shù)組里面是實(shí)際被刪除的元素如果沒刪除任何元素返回空數(shù)組。splice也是原地修改。start參數(shù)支持負(fù)數(shù)比如arr.splice(-1, 0, x)表示在倒數(shù)第一個(gè)元素之前插入等于把數(shù)組長(zhǎng)度加一新元素處在倒數(shù)第二位。負(fù)數(shù)規(guī)則和slice一致但如果對(duì)負(fù)數(shù)場(chǎng)景不熟很容易搞混建議先打印測(cè)試一遍再寫進(jìn)代碼。splice在中間位置插入的成本取決于插入點(diǎn)越靠前需要向后搬移的元素越多越靠后搬移越少。最壞情況是在 0 號(hào)位置插入復(fù)雜度同樣是 O(n)和unshift一個(gè)量級(jí)。所以它不是一個(gè)“想插哪就隨手插”的方法數(shù)據(jù)量大的時(shí)候要慎重。2.4concat無副作用的拼接concat的本意是連接多個(gè)數(shù)組或值語法是arr.concat(value1, ..., valueN)。如果參數(shù)是數(shù)組它會(huì)把數(shù)組元素打平一層加進(jìn)去如果參數(shù)是普通值就作為單個(gè)元素追加。關(guān)鍵點(diǎn)concat不會(huì)修改原數(shù)組而是返回一個(gè)新數(shù)組。這是添加元素時(shí)最常用的不可變方案之一。比如arr.concat(4, [5, 6])得到一個(gè)新數(shù)組[舊元素..., 4, 5, 6]原數(shù)組保持原樣。注意它只展開數(shù)組第一層如果參數(shù)是嵌套數(shù)組比如arr.concat([1, [2]])最終結(jié)果是[..., 1, [2]]內(nèi)層嵌套會(huì)被保留。因?yàn)閏oncat內(nèi)部要遍歷原數(shù)組的所有元素再拼接上新元素所以整體時(shí)間復(fù)雜度是 O(n)。這個(gè) n 包含原數(shù)組長(zhǎng)度。也就是說在 10 萬個(gè)元素的數(shù)組后面加 1 個(gè)元素concat要復(fù)制十萬個(gè)元素到一個(gè)新數(shù)組里而不是在原數(shù)組上追加一個(gè)。性能自然比push差但它換來的是原數(shù)組不被污染在不可變數(shù)據(jù)場(chǎng)景里值這個(gè)代價(jià)。2.5 展開運(yùn)算符更現(xiàn)代的不可變寫法ES6 之后展開運(yùn)算符成了構(gòu)造新數(shù)組最受歡迎的寫法。尾部添加是[...arr, newItem]頭部添加是[newItem, ...arr]中間插入可以配合slice[...arr.slice(0, i), item, ...arr.slice(i)]。它和concat一樣不修改原數(shù)組但可讀性更強(qiáng)而且能直接用在數(shù)組字面量里。不過要注意展開運(yùn)算符的適用范圍是“可迭代對(duì)象”不只是數(shù)組。字符串可以被展開成字符數(shù)組Set、Map 也可以。如果想展開一個(gè)類數(shù)組對(duì)象比如arguments直接展開是可行的因?yàn)閍rguments是可迭代的但如果是一個(gè)純粹的類數(shù)組比如{0: a, length: 1}它其實(shí)不能用...直接展開這時(shí)往往需要Array.from。展開運(yùn)算符的底層邏輯也是“復(fù)制整個(gè)數(shù)組再添加”復(fù)雜度 O(n)。在 React 的useState更新列表時(shí)我用得最多的就是[...prev, newItem]它比concat更直觀也避免了原地修改 state 的坑。2.6 直接利用length和索引最樸素的底層寫法嚴(yán)格說這不算一個(gè)新 API而是一類底層技巧通常有兩種寫法第一種是arr[arr.length] value。因?yàn)閿?shù)組索引從 0 開始arr.length始終指向最后一個(gè)元素后一格的空位所以這種寫法等同于push但返回值不同push返回新length這種賦值返回value。在微基準(zhǔn)測(cè)試?yán)锼袝r(shí)候比push更快因?yàn)樗×艘淮魏瘮?shù)調(diào)用不過差距非常小。第二種是直接改arr.length。比如arr.length arr.length 1這會(huì)往數(shù)組末尾追加一個(gè)空槽empty slot數(shù)組長(zhǎng)度變大但值沒有初始化或者arr.length 0可以清空數(shù)組。利用length給數(shù)組“擴(kuò)容”添加空槽的做法我不推薦在業(yè)務(wù)代碼里用因?yàn)榭詹蹠?huì)造成非常隱蔽的問題數(shù)組方法在處理空槽時(shí)行為不一致map、filter、forEach會(huì)跳過空槽但some和find又會(huì)把它當(dāng)作undefined去執(zhí)行。后面第 5 章我會(huì)寫一個(gè)真實(shí)踩坑案例。3. 性能實(shí)測(cè)十萬次添加操作結(jié)果可能顛覆直覺3.1 測(cè)試代碼怎么設(shè)計(jì)才公平網(wǎng)上有很多數(shù)組性能對(duì)比的代碼但不少寫得不太公平。比如測(cè)concat和展開運(yùn)算符時(shí)有人直接寫成console.time(concat); for (let i 0; i 100000; i) { arr.concat(i); } console.timeEnd(concat);這里的問題在于每次concat的返回值沒有賦值回去結(jié)果concat雖然復(fù)制了數(shù)組但復(fù)制出來的新數(shù)組被丟掉了原數(shù)組還是最初那一個(gè)循環(huán)多少次復(fù)制的都是同一個(gè)長(zhǎng)度的數(shù)組。但在真實(shí)使用場(chǎng)景里你會(huì)把新數(shù)組重新賦值回去數(shù)組會(huì)不斷變大所以更公平的測(cè)試要模擬真實(shí)的累積操作。我這里提供一個(gè)簡(jiǎn)化版但相對(duì)公平的基準(zhǔn)測(cè)試思路const N 100000; const rounds 5; function testPush() { let arr []; const start performance.now(); for (let i 0; i N; i) arr.push(i); return performance.now() - start; } function testIndexWrite() { let arr []; const start performance.now(); for (let i 0; i N; i) arr[arr.length] i; return performance.now() - start; } function testUnshift() { let arr []; const start performance.now(); for (let i 0; i N; i) arr.unshift(i); return performance.now() - start; } function testSpliceEnd() { let arr []; const start performance.now(); for (let i 0; i N; i) arr.splice(arr.length, 0, i); return performance.now() - start; } function testSpliceStart() { let arr []; const start performance.now(); for (let i 0; i N; i) arr.splice(0, 0, i); return performance.now() - start; } function testConcatEnd() { let arr []; const start performance.now(); for (let i 0; i N; i) arr arr.concat(i); return performance.now() - start; } function testSpreadEnd() { let arr []; const start performance.now(); for (let i 0; i N; i) arr [...arr, i]; return performance.now() - start; }跑的時(shí)候每個(gè)函數(shù)各執(zhí)行rounds次取中位數(shù)更能反映真實(shí)情況。注意先把 N 設(shè)小一點(diǎn)比如 10000 跑通再根據(jù)機(jī)器調(diào)整否則unshift和concat那一組可能會(huì)讓你等到懷疑人生。3.2 實(shí)測(cè)結(jié)果和解讀在我這邊 Node 20 環(huán)境下N100000、rounds5 的前提下結(jié)果大概是這樣的數(shù)值僅供參考不同機(jī)器會(huì)有浮動(dòng)但相對(duì)關(guān)系很穩(wěn)定方法操作位置是否修改原數(shù)組平均耗時(shí)10萬次復(fù)雜度arr[arr.length] i尾部是約 2msO(1)push(i)尾部是約 3msO(1)splice(arr.length, 0, i)尾部是約 12ms平均 O(1)但有函數(shù)調(diào)用開銷unshift(i)頭部是約 950msO(n)splice(0, 0, i)頭部是約 1500msO(n)concat(i)尾部否約 3800msO(n)[...arr, i]尾部否約 4000msO(n)更直觀一點(diǎn)push和直接索引賦值跑完只要幾毫秒unshift要做元素搬移直接到了秒級(jí)。而concat和展開運(yùn)算符因?yàn)槊看窝h(huán)都復(fù)制整個(gè)不斷變大的數(shù)組耗時(shí)呈現(xiàn)累加的二次方增長(zhǎng)所以比預(yù)期還要慢很多。這個(gè)結(jié)果不是特地去調(diào)出來的。背后的規(guī)律就是只要復(fù)制整個(gè)數(shù)組n 越大越吃虧只要頭部插入需要搬移所有元素n 越大越吃虧。唯一接近“免罪”的尾部原地操作因?yàn)椴恍枰嵋圃夭艜?huì)在長(zhǎng)數(shù)組上遙遙領(lǐng)先。3.3 大數(shù)組批量添加時(shí)的參數(shù)展開陷阱如果測(cè)試?yán)锇蜒h(huán)改成push(...bigArray)這種寫法會(huì)掉進(jìn)另一個(gè)坑。JS 引擎對(duì)函數(shù)參數(shù)數(shù)量是有限制的當(dāng)展開的數(shù)組元素?cái)?shù)量過大在 V8 中大約超過 65535 個(gè)就會(huì)引發(fā)RangeError: Maximum call stack size exceeded。unshift(...bigArray)和splice(0, 0, ...bigArray)同理。所以批量往數(shù)組里塞大量數(shù)據(jù)的時(shí)候不要圖省事用展開語法當(dāng)參數(shù)。推薦下面這幾種方式原數(shù)組可以被修改直接for循環(huán)push想保持不可變使用arr.concat(bigArray)concat能接收數(shù)組作為參數(shù)不需要把數(shù)組展開成參數(shù)列表如果必須用push批量追加可以arr.push(...bigArray.slice(0, 5000))分段展開再繼續(xù)下一段但這只是繞開參數(shù)上限的方案不是最優(yōu)我記得有一次線上數(shù)據(jù)同步功能就是因?yàn)樵谘h(huán)里用了push(...chunk)chunk 一次一萬條沒事某天業(yè)務(wù)方給了個(gè)五萬條的 chunk然后就報(bào)錯(cuò)中斷了。從那以后我對(duì)“展開給函數(shù)傳參”都多留了一份心眼。4. 為什么 push 比 unshift 快那么多V8 引擎的數(shù)組存儲(chǔ)機(jī)制4.1 快數(shù)組和慢數(shù)組要理解性能差異得看點(diǎn)引擎內(nèi)部的東西。V8 里面數(shù)組最常見的是“快數(shù)組”Fast Elements它的元素存儲(chǔ)在連續(xù)內(nèi)存上索引直接對(duì)應(yīng)內(nèi)存偏移訪問復(fù)雜度 O(1)??鞌?shù)組內(nèi)部還會(huì)根據(jù)數(shù)組里存的元素類型做更細(xì)的分類比如全是整數(shù)、全是 double、全是對(duì)象、混合類型不同的 ElementsKind 對(duì)應(yīng)不同的存儲(chǔ)和優(yōu)化策略。這也是為什么如果你真的追求性能盡量保持?jǐn)?shù)組元素類型一致。當(dāng)數(shù)組變得非常稀疏、或者索引跳躍太離譜V8 可能把它降級(jí)成“慢數(shù)組”Dictionary Elements本質(zhì)上是哈希表結(jié)構(gòu)索引變成長(zhǎng)度很長(zhǎng)的 key。這種數(shù)組元素訪問不再是簡(jiǎn)單偏移性能會(huì)下降很多。所以在業(yè)務(wù)代碼里動(dòng)不動(dòng)就arr[100000] 1這樣的寫法可能讓數(shù)組直接變成字典模式后續(xù)所有操作都變慢。4.2 unshift 的底層搬移過程unshift為什么慢本質(zhì)上就是“騰位置”。假設(shè)現(xiàn)在快數(shù)組里有[a, b, c, d]內(nèi)存里是連續(xù)的一塊。要在頭部插入 x引擎不能像鏈表那樣只改指針?biāo)仨毎?a、b、c、d 這四個(gè)元素整體往后移動(dòng)一個(gè)槽位再把 x 放到 0 號(hào)位置。這個(gè)搬移是逐元素 copyn 個(gè)元素就要搬 n 次數(shù)組長(zhǎng)度越大耗時(shí)越長(zhǎng)。splice在頭部插入也是同樣的搬移邏輯所以兩個(gè)方法在頭部場(chǎng)景下都是 O(n)。有趣的是push和arr[arr.length]之所以快是因?yàn)樗鼈冊(cè)跀?shù)組尾部寫入不需要搬移任何已有元素最多只是數(shù)組本身的容量不夠時(shí)觸發(fā)擴(kuò)容。V8 的擴(kuò)容是成倍數(shù)增長(zhǎng)均攤到每次添加是常數(shù)時(shí)間整體就是 O(1)。4.3 中間插入其實(shí)是“半截搬家”splice在中間插入的執(zhí)行過程類似從插入位置開始把后半段元素往后挪一位再把新元素填進(jìn)去。涉及的元素?cái)?shù)量不是整個(gè)數(shù)組而是插入位置后面的那一截。所以它的平均復(fù)雜度也是 O(n)但常數(shù)系數(shù)比unshift小。數(shù)組長(zhǎng)度為 n 時(shí)插入在頭部要搬 n 個(gè)元素插入在中間平均搬 n/2 個(gè)插入在尾部幾乎不用搬。這也解釋了測(cè)試中splice(arr.length, 0, i)為什么比splice(0, 0, i)快得多。想驗(yàn)證這個(gè)結(jié)論也很簡(jiǎn)單在插入位置附近打印一下下標(biāo)或者用不同位置各跑一遍基準(zhǔn)。實(shí)際項(xiàng)目中如果必須在中間大量插入元素可以考慮先把數(shù)組拆成前后兩段分別組合或者干脆換數(shù)據(jù)結(jié)構(gòu)。4.4 不可變操作的成本被很多人低估concat和展開運(yùn)算符表面上只是“方便地返回新數(shù)組”但這個(gè)新數(shù)組不是憑空出現(xiàn)的。它需要先根據(jù)原數(shù)組長(zhǎng)度申請(qǐng)一塊新內(nèi)存然后把原數(shù)組的元素一個(gè)個(gè)復(fù)制過去再追加新元素。這意味著每次操作都要遍歷 n 個(gè)舊元素。如果你在一個(gè)循環(huán)里不斷往同一個(gè)數(shù)組尾部添加每次數(shù)組長(zhǎng)度都會(huì)增加每次都要復(fù)制更大的數(shù)組總耗時(shí)接近 O(n2)。所以如果數(shù)據(jù)量大又堅(jiān)持不可變操作性能通常不會(huì)好看。實(shí)際工程中我們要做的不是“永遠(yuǎn)不用concat”而是知道在什么臨界點(diǎn)開始需要思考優(yōu)化。比如一個(gè)列表只有幾百條spread 怎么寫都無所謂一旦上萬條且頻繁更新就應(yīng)該考慮改成增量更新或者用 immer 這類工具來管理狀態(tài)而不是硬扛著每次復(fù)制大數(shù)組。5. 真實(shí)項(xiàng)目里的選型建議與避坑指南5.1 不同場(chǎng)景應(yīng)該選哪個(gè)方法根據(jù)自己的項(xiàng)目和踩坑經(jīng)歷我做了一個(gè)簡(jiǎn)單的決策對(duì)照表場(chǎng)景推薦寫法原因純粹在尾部追加原數(shù)組可修改arr.push(item)性能最好語義清晰尾部追加但不想引入副作用[...arr, item]或arr.concat(item)不改變?cè)瓟?shù)組適合 state 更新偶爾在頭部插入數(shù)組不大arr.unshift(item)代碼可讀性最好性能可接受頻繁在頭部插入數(shù)組很大改用尾部push 最后reverse或換數(shù)據(jù)結(jié)構(gòu)避免每次 O(n) 搬移任意位置單次插入arr.splice(index, 0, item)功能完整注意 index 越界不可變地任意位置插入[...arr.slice(0, i), item, ...arr.slice(i)]不污染原數(shù)組可讀性好合并多個(gè)大數(shù)組arr.concat(bigArr1, bigArr2)避免參數(shù)展開上限問題決策時(shí)我一般只問三個(gè)問題第一能不能修改原數(shù)組不能直接進(jìn)concat/spread 分支第二數(shù)據(jù)量大概多大超過一萬條就要開始警惕 O(n) 操作第三操作位置在哪頭部和中間插入都是 O(n)要盡量規(guī)避。5.2 我踩過的幾個(gè)典型坑先說最經(jīng)典的一個(gè)用length賦值制造稀疏數(shù)組。let arr [1, 2, 3]; arr.length 5;此時(shí) arr 變成[1, 2, 3, 2 empty items]。如果直接arr.map(x x * 2)結(jié)果會(huì)是[2, 4, 6, 2 empty items]右側(cè)空槽依然存在且被跳過很多人在這一步以為map出 bug 了。更詭異的是arr.forEach不會(huì)遍歷空槽但arr.find會(huì)認(rèn)為空槽是undefined行為不一致會(huì)帶來非常難排查的數(shù)據(jù)問題。所以除非你能確定空槽不影響業(yè)務(wù)否則別用length賦空槽。第二個(gè)坑是 React 等框架里的 state 數(shù)組直接push。我見過不止一次這樣的代碼const [list, setList] useState([]); function addItem(item) { list.push(item); setList(list); }list 的引用沒變React 做淺比較的時(shí)候會(huì)認(rèn)為 state 沒變組件不重渲染。正確寫法是setList([...list, item])或setList(list.concat(item))。這個(gè)坑背后就是第 1 章說的“原地修改 vs 返回新數(shù)組”的分類問題面試官愛考它是有道理的。第三個(gè)坑是批量插入用了展開運(yùn)算符當(dāng)參數(shù)。前面 3.3 節(jié)也說過push(...bigArray)存在參數(shù)上限的問題在大數(shù)據(jù)量時(shí)直接拋異常。一旦發(fā)現(xiàn)問題換成concat或者循環(huán)push就穩(wěn)多了。5.3 如果真的要頻繁首尾插數(shù)組不是好選擇當(dāng)你確認(rèn)業(yè)務(wù)就是高頻地在數(shù)組頭部或尾部插入并且數(shù)據(jù)量很大光靠“用push代替unshift”是不夠的。一個(gè)常見做法是維護(hù)“雙端隊(duì)列”的思維內(nèi)部用兩個(gè)數(shù)組一個(gè)正向存后半段一個(gè)反向存前半段。往頭部插入時(shí)插到反向數(shù)組的尾部往尾部插入時(shí)插到正向數(shù)組的尾部需要讀取時(shí)把反向數(shù)組反轉(zhuǎn)后和正向數(shù)組合并。這樣頭部插入也變成 O(1)。另一個(gè)更簡(jiǎn)單粗暴的替代方案實(shí)在不行用循環(huán)數(shù)組或者單向鏈表。JS 沒有原生鏈表但你完全可以用對(duì)象模擬一個(gè)節(jié)點(diǎn)結(jié)構(gòu)或者引入現(xiàn)成的數(shù)據(jù)結(jié)構(gòu)庫。不過要注意鏈表犧牲了隨機(jī)訪問能力如果業(yè)務(wù)上既要隨機(jī)訪問又要頻繁頭插那就要權(quán)衡了。我自己的經(jīng)驗(yàn)是大多數(shù)業(yè)務(wù)場(chǎng)景其實(shí)頭插頻率沒那么高真有那么高的時(shí)候重新審視一遍數(shù)據(jù)模型往往能找到一個(gè)可以reverse或分段處理的方案。提示別做“為了優(yōu)化而優(yōu)化”的事情。如果你的數(shù)組只有幾十上百個(gè)元素unshift和splice隨便用肉眼根本感覺不到區(qū)別。先把代碼可讀性保住等到性能瓶頸真實(shí)出現(xiàn)再按本章思路去改。我個(gè)人在項(xiàng)目里的習(xí)慣是默認(rèn)情況下一律用push需要不可變更新時(shí)用展開運(yùn)算符偶爾插中間用splice但會(huì)先把數(shù)據(jù)規(guī)模放在心里。遇到需要頻繁頭插的大數(shù)組第一反應(yīng)不是急著換個(gè)方法而是先懷疑數(shù)據(jù)模型是否合理。把這三件事想明白數(shù)組添加元素這件事基本就不會(huì)再踩坑了。