前言

在 JavaScript 開發中,陣列操作是不可或缺的一環,而「陣列合併」更是經常出現的需求。在幾年前我寫過筆記 https://tech.simonallen.app/es6-spread-operator-array/,但進入現職公司後,我反而看到的都是 concat()

其實不論是在資料處理、狀態更新,還是函式參數傳遞時,我們都會面臨選擇:該用 concat() 方法,還是使用語法更簡潔的 ... Spread Operator?

先說結論,以我目前觀看台灣的技術筆記和公司程式碼、社群朋友習慣,大家都偏好 Spread Operator,如果沒有特殊用途那我建議使用 Spread Operator。不過這篇文章還是會從語法可讀性、使用彈性、效能表現以及實務場景等面向來比較。

兩種合併方法介紹

Array.concat()

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = arr1.concat(arr2);
console.log(merged); // [1, 2, 3, 4, 5, 6]

concat() 方法會回傳一個新的陣列,也就是說他是 Immutable,不會修改原始資料,並且支援合併多個陣列與非陣列元素。

Spread Operator

Google 的 JavaScript Style Guide 是這樣說的

Array literals may include the spread operator (...) to flatten elements out of one or more other iterables. The spread operator should be used instead of more awkward constructs with Array.prototype. There is no space after the ....

展開運算子 ... 是 ES2015+ 以後引入的語法,除了用在陣列合併,也能用於函式參數、物件合併,是目前主流常見的選擇,範例:

const merged = [...arr1, ...arr2];
console.log(merged); // [1, 2, 3, 4, 5, 6]

使用語意與彈性比較

Spread Operator

除了合併陣列外,還能搭配函式、物件、甚至生成器使用:

const fn = (...args) => console.log(args);
fn(...[1, 2, 3]); // [1, 2, 3]

const obj = { ...{ a: 1 }, b: 2 }; // { a: 1, b: 2 }

語意上非常直觀,攤平然後 inset 插進去。

concat

const base = [1, 2, 3];
const str = 'hello';
console.log(base.concat(str));    // [1, 2, 3, 'hello']
console.log([...base, ...str]);   // [1, 2, 3, 'h', 'e', 'l', 'l', 'o']

const num = 99;
console.log(base.concat(num));    // [1, 2, 3, 99]
console.log([...base, ...num]);   // TypeError: num is not iterable

concat() 會把非陣列元素直接當作單一元素加進去,而 ... 則嘗試「展開」,不能被 loop 的資料(像數字)會報錯。這個差異在處理非標準資料來源時要特別留意。

淺層合併與深拷貝

無論是 concat() 還是 ...,都僅支援淺拷貝。

const a = [1, { x: 2 }];
const b = [{ y: 3 }];
const merged = a.concat(b);
// 或 [...a, ...b]

merged[1].x = 99;
console.log(a[1].x); // 99(原本的物件被改到)

如果需要的是深拷貝或攤平多層巢狀陣列,可能需要額外使用 ㄑ宜他方法。

效能差異真的大嗎?

先說結論:在小型或中型資料結構中,兩者差異極小,不需過度最佳化。

按照我爬到的文章資料,在大陣列中,concat() 有時效能略優,尤其是 JS V8 引擎對 concat() 說什麼有較多底層最佳化。但也有測試顯示 Spread 更快,這還是取決於執行環境與版本。

實務建議:以語意與可讀性為主要考量。真的需要檢查效能應該是看對整體資料結構的操作,例如過度使用 Immutable 來重複 loop 建立新資料、沒有減少 loop 時機,例如什麼 .map().filter().concat() 這樣一直串下去,結果某些 loop 可以和其他合併在一起,也說不定 .reduce()就能解決,搞不好 hashmap、hashtable 也能解決。

前端開發場景怎麼選?

React / Vue 等框架的資料更新

React 的 setState 通常會使用展開語法產生新資料,避免修改舊狀態:

this.setState({
  items: [...this.state.items, newItem]
});

這裡 ... 語法更直覺且好讀。

多筆資料合併

假設要合併資料:

const combined = [...remoteData, ...localCache];

此時 Spread 的語意是「資料來源展開進來」,更貼近開發者心智。

處理非陣列元素

如果資料來源可能混雜不同型別,concat() 更穩定:

const arr = [1, 2, 3];
const mixed = arr.concat(99, 'hello', [4, 5]);
// => [1, 2, 3, 99, 'hello', 4, 5]

Spread 則需要保證所有元素都是 iterable,否則容易出錯。

generator 資料處理

如果資料來自 generator,... 支援展開:

function* gen() { yield* 'abc'; }
console.log([...arr, ...gen()]); // ['1', 2, 3, 'a', 'b', 'c']

concat() 只會加進 generator 物件本身,不會展開。

瀏覽器相容性

可以到 can i use 檢查:https://caniuse.com/?search=...

... 是 ES2015+ 語法,現代環境已普遍支援,但若要支援 IE11 或更舊環境,建議使用 Babel 或選用 concat()但現在誰還支援 IE,所以放心用吧。

結語

兩種合併方式本質相似,但在語法、資料型別處理與實務彈性上還是有許多細節值得注意:

  • (相對)現代風格:使用 Spread Operator。
  • 資料型別不確定或需安全處理:可考慮 concat。
  • 效能敏感情境:建議自行 benchmark,不能一概而論。
  • 舊環境相容需求:concat 會是更穩定的選項。

沒有哪個比較好,只有哪個「在當下場景更適合」。養成根據語意選擇工具的習慣,才是寫出高可讀、可維護程式的關鍵。

參考資料

https://stackoverflow.com/questions/48865710/spread-operator-vs-array-concat

Spread syntax (...) - JavaScript | MDN
The spread (...) syntax allows an iterable, such as an array or string, to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected. In an object literal, the spread syntax enumerates the properties of an object and adds the key-value pairs to the object being created.
Array.prototype.concat() - JavaScript | MDN
The concat() method of Array instances is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
Google JavaScript Style Guide