JavaScript 課程 – 運算子(Operators)
主題:展開運算子(Spread Operator ...)
簡介
在 ES6 之後,展開運算子(Spread Operator)成為 JavaScript 開發者日常工具箱中不可或缺的一環。它以簡潔的語法,讓我們可以快速展開(spread)陣列、物件或可迭代物件的內容,從而達成複製、合併、參數展開等多種需求。
對於剛踏入前端開發的同學來說,展開運算子不僅能讓程式碼更易讀,也能減少繁瑣的 Array.prototype.concat、Object.assign 或 for...of 迴圈寫法;對於已有開發經驗的中階工程師,則能在函式式編程、不可變資料結構(immutable data)以及 React/Vue 等框架的狀態管理中,提供更安全且效能友善的解法。
本篇文章將從 概念、語法、實作範例 一路說到 常見陷阱與最佳實踐,最後以 實際應用場景 作結,幫助你在專案中熟練運用展開運算子,寫出更簡潔、可維護的程式碼。
核心概念
1. 什麼是展開運算子?
展開運算子使用三個連續的點號 ...,其作用是把一個可迭代物件的每個元素「展開」成獨立的值。簡單來說,它把「容器」裡的內容「倒出」來,讓其他語法(如陣列字面量、函式呼叫、物件字面量)直接接受。
const numbers = [1, 2, 3];
console.log(...numbers); // 1 2 3
註:在
console.log中使用...只是一種示範,實際上...必須出現在語法允許「展開」的位置(陣列、函式、物件等),否則會拋出 SyntaxError。
2. 展開運算子可用於哪些資料型別?
| 資料型別 | 是否支援展開 | 常見使用情境 |
|---|---|---|
| Array | ✅ | 合併陣列、複製陣列、展開參數 |
| String | ✅(會以字元為單位) | 展開字串成字元陣列 |
| Map / Set | ✅(透過 Array.from 或直接展開) |
轉換為陣列或合併集合 |
| Object | ✅(ES2018+) | 合併物件、淺層複製 |
| TypedArray | ✅ | 與一般陣列互換 |
| Arguments | ✅(在函式內) | 轉換為真正的陣列 |
注意:展開運算子只能作用在**可迭代物件(iterable)或純物件(plain object)**上。若傳入
null、undefined或非迭代物件,會直接拋出TypeError。
3. 基本語法
| 使用情境 | 語法範例 |
|---|---|
| 陣列合併 | const merged = [...arr1, ...arr2]; |
| 函式參數展開 | fn(...argsArray); |
| 物件合併(淺層) | const mergedObj = {...obj1, ...obj2}; |
| 字串展開為陣列 | const chars = [..."hello"]; // ["h","e","l","l","o"] |
程式碼範例
以下提供 5 個實務導向的範例,每個範例都附上註解說明,幫助你快速掌握展開運算子的多樣用法。
範例 1️⃣:陣列的深拷貝(淺層拷貝)
// 原始陣列
const original = [1, 2, { name: "Alice" }];
// 使用展開運算子建立新陣列(淺層拷貝)
const copy = [...original];
// 修改 copy 中的 primitive 元素不會影響 original
copy[0] = 99;
console.log(original[0]); // 1
// 但若修改物件屬性,兩者仍指向同一個參考
copy[2].name = "Bob";
console.log(original[2].name); // "Bob"
重點:展開運算子只能做到淺層拷貝,若陣列內有物件或陣列,需要使用
JSON.parse(JSON.stringify(...))或深拷貝函式。
範例 2️⃣:函式參數的展開(可變長度參數)
function sum(...values) {
// values 會自動成為一個陣列
return values.reduce((total, cur) => total + cur, 0);
}
// 從已有的陣列展開成單獨參數傳入
const numbers = [4, 7, 2];
console.log(sum(...numbers)); // 13
說明:
...values同時具備 剩餘參數(rest parameter) 與 展開運算子 的功能。它把傳入的所有參數收集成陣列,讓reduce可以直接處理。
範例 3️⃣:物件的合併與覆寫(淺層合併)
const defaults = {
host: "localhost",
port: 80,
protocol: "http"
};
const userConfig = {
port: 8080, // 想要覆寫預設的 port
protocol: "https"
};
// 使用展開運算子合併,後面的屬性會覆寫前面的
const finalConfig = { ...defaults, ...userConfig };
console.log(finalConfig);
// { host: 'localhost', port: 8080, protocol: 'https' }
技巧:合併順序很重要,後面的物件會覆寫前面的屬性,這在設定預設值時非常好用。
範例 4️⃣:在 React 中使用展開運算子傳遞 Props
function Button({ type, disabled, children, ...rest }) {
return (
<button type={type} disabled={disabled} {...rest}>
{children}
</button>
);
}
// 使用時只需傳入需要的屬性,其他全部透過展開自動加入
<Button
type="submit"
className="primary"
onClick={() => console.log('clicked')}
>
送出
</Button>
說明:在 React 的 JSX 中,
{...rest}會把rest物件裡的每個屬性展開成獨立的props,讓組件更具彈性且避免手動列舉多餘屬性。
範例 5️⃣:利用 Set & 展開運算子去除陣列重複項
const raw = [1, 2, 3, 2, 4, 1, 5];
// 先把陣列轉成 Set(自動去重),再展開回陣列
const unique = [...new Set(raw)];
console.log(unique); // [1, 2, 3, 4, 5]
原理:
Set只允許唯一值,透過...new Set()把它「倒出」成普通陣列,就完成了 O(n) 的去重作業。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
| 1️⃣ 展開非 iterable 會拋錯 | ...null、...undefined、...42 都會產生 TypeError。 |
在使用前先確保資料是 iterable,或使用 Array.isArray、typeof 檢查。 |
| 2️⃣ 淺層拷貝的誤解 | 以為 ...obj 可以深拷貝巢狀物件。 |
若需要深拷貝,使用 structuredClone(ES2021)或自行實作遞迴拷貝。 |
| 3️⃣ 展開順序不當 | 合併物件或陣列時,順序決定了最終結果。 | 明確寫出「預設 → 使用者設定」的順序,或使用 Object.assign 以同樣概念。 |
| 4️⃣ 在函式呼叫時忘記展開 | fn(argsArray) 只會把陣列當作單一參數。 |
必須寫成 fn(...argsArray) 才會把每個元素當作獨立參數。 |
| 5️⃣ 展開大量資料造成效能問題 | 大型陣列或物件在展開時會產生臨時副本,佔用記憶體。 | 只在需要時使用,對於巨量資料考慮使用迭代器或 for...of。 |
最佳實踐小結
- 保持不可變(immutability):使用展開運算子產生新陣列/物件,避免直接改變原始資料。
- 明確命名:如
const newState = {...oldState, updatedProp},讓程式碼可讀性提升。 - 結合其他語法:
Array.from,Object.entries,Set等,都可以與展開運算子搭配,寫出更簡潔的資料轉換流程。 - 注意淺層拷貝:若涉及巢狀結構,務必在文件或註解中說明拷貝層級,防止意外的副作用。
實際應用場景
1️⃣ Redux / Vuex 狀態更新
在 Redux reducer 中,我們常用展開運算子產生新狀態:
function todosReducer(state = [], action) {
switch (action.type) {
case "ADD_TODO":
return [...state, action.payload]; // 加入新項目
case "REMOVE_TODO":
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
2️⃣ API 請求參數組合
當需要把多個查詢條件合併成一個物件傳給 fetch:
const pagination = { page: 1, limit: 20 };
const filters = { status: "active", category: "tech" };
const query = { ...pagination, ...filters };
// => { page:1, limit:20, status:"active", category:"tech" }
fetch(`/api/articles?${new URLSearchParams(query)}`);
3️⃣ 動態建立 UI 元件
在 UI 框架中,常透過展開運算子把「共用屬性」與「個別屬性」合併:
const baseProps = { className: "btn", type: "button" };
const primaryProps = { ...baseProps, className: "btn primary" };
<Button {...primaryProps}>Primary</Button>
4️⃣ 資料搬移(Data Migration)
在資料庫遷移或資料清理腳本中,使用展開運算子快速重組物件:
const oldRecord = { id: 1, name: "John", age: 30, extra: "unused" };
const { extra, ...newRecord } = oldRecord; // 去除不需要的欄位
// newRecord => { id:1, name:"John", age:30 }
總結
展開運算子 ... 是 ES6 以後最具表現力的語法糖,它讓我們能以直觀、可讀的方式處理陣列、字串、Set、Map 以及物件的展開、合併與複製。
- 概念:把可迭代或純物件的內容「倒出」成獨立值。
- 核心語法:
[...arr]、{...obj}、fn(...args)。 - 實務範例:從陣列深拷貝、函式參數展開、React Props、去重、狀態管理等皆可見其身影。
- 陷阱:非 iterable 的展開會錯、淺層拷貝的限制、展開順序的影響等。
- 最佳實踐:保持資料不可變、明確命名、配合其他迭代工具、留意效能。
掌握展開運算子後,你將能在 寫程式的速度、可讀性與維護成本 上獲得顯著提升。建議在日常開發中有意識地替換掉冗長的 concat、Object.assign 或手寫迴圈,讓程式碼更貼近 「資料即語意」 的設計哲學。祝你寫程式愉快,持續在 JavaScript 的世界裡探索更高效的寫法! 🚀