不必使用 ... 擴展運算符,這三個場景下性能極差
擴展運算符無疑是我們 JavaScript 開發者最喜愛的語法糖之一。
其代碼優雅、簡潔,可讀性極高,并且完美契合了函數式編程和不可變性的思想。
然而,在某些特定場景下,擴展運算符可能會導致嚴重的性能瓶頸,甚至拖垮我們的應用。

... 的本質
對于數組或對象,擴展運算符會:
- 創建一個新的空數組或空對象
- 遍歷原始數據結構的所有可枚舉屬性(對于對象)或所有值(對于數組)
- 將遍歷到的每一個值逐一復制到這個新的數據結構中
關鍵點在于:創建新對象/數組 和 遍歷并復制。當數據量很小的時候,這點開銷微不足道,但當數據量變大或操作頻率變高時,問題就暴露了。
場景一:操作大型數組
這是最常見也最容易被忽視的性能陷阱,假設你有一個包含數百萬條記錄的數組(例如,從后端獲取的海量數據),你只是想在末尾添加一個新元素。
// 性能不佳的寫法
const hugeArray = Array(1_000_000).fill(0);
const newItem = 1;
const newArray = [...hugeArray, newItem]; // ?? 問題所在為了添加 newItem,擴展運算符創建了一個全新的數組 newArray,然后把 hugeArray 中的 一百萬個元素 全部循環復制了一遍,最后才把 newItem 放進去。這個操作的時間復雜度是 O(n),其中 n 是數組的長度。每次添加,都意味著一次完整的大規模內存分配和數據拷貝。
如果你的場景允許直接修改原數組(即不需要保持不可變性),使用 push() 方法是最高效的。

push() 的時間復雜度是 O(1)(攤銷分析),它直接在數組的末尾添加元素,幾乎沒有額外開銷。兩者性能相差成百上千倍。
場景二:在循環中頻繁更新狀態
這個場景在 Redux 的 Reducer 或其他狀態管理模式中尤為常見,開發者為了保持狀態的不可變性,在循環中反復使用擴展運算符。
假設我們需要根據一個 ID 列表來構建一個對象映射(Map)。
const ids = ['id_1', 'id_2', /* ...成千上萬個ID */];
// 性能不佳的寫法
const userMap = ids.reduce((acc, id) => {
// 每次循環都創建一個新對象,并復制所有已有屬性
return {
...acc, // ?? 問題所在
[id]: { name: `User ${id}` }
};
}, {});這個 reduce 操作看起來很函數式,很“正確”,但性能卻是一場災難。
- 第一次循環,acc 是 {},返回 { id_1: ... }。
- 第二次循環,acc 是 { id_1: ... },為了加入 id_2,它會創建一個新對象,復制 id_1 的所有內容,再添加 id_2。
- 第三次循環,它會創建一個新對象,復制 id_1 和 id_2 的內容,再添加 id_3。
- …
- 第 n 次循環,它會復制 n-1 個已有屬性。
這個過程的總操作次數大致是 1 + 2 + … + (n-1),時間復雜度高達 O(n2)。同時,它在內存中創建了大量的臨時對象,給垃圾回收(GC)帶來了巨大壓力。
在循環內部,使用一個可變對象進行操作,循環結束后再返回最終結果。這并不會破壞外部的不可變性。

或者使用更直觀的 for 循環
場景三:將類數組對象(Array-like Object)轉換為數組
在處理 arguments、NodeList、HTMLCollection 等類數組對象時,用擴展運算符轉換成真數組非常方便。
function processArgs() {
const args = [...arguments]; // 看起來很簡潔
// ...
}
const elements = document.querySelectorAll('div');
const elementArray = [...elements]; // 同樣簡潔雖然這種方式在大多數情況下性能尚可,但 JavaScript 引擎對 Array.from() 做了專門的優化,它不僅性能更好,語義也更清晰:明確表示“從一個類數組/可迭代對象創建一個新數組”。

絕大多數使用擴展運算符的地方都不會構成性能問題。但是,當需要處理海量數據、高頻更新或性能敏感的核心邏輯時,考慮下 ... 是否正在悄悄地拖慢我們的程序。



























