本文 AI 產出,尚未審核

JavaScript 課程 – 運算子(Operators)

主題:展開運算子(Spread Operator ...


簡介

在 ES6 之後,展開運算子(Spread Operator)成為 JavaScript 開發者日常工具箱中不可或缺的一環。它以簡潔的語法,讓我們可以快速展開(spread)陣列、物件或可迭代物件的內容,從而達成複製、合併、參數展開等多種需求。

對於剛踏入前端開發的同學來說,展開運算子不僅能讓程式碼更易讀,也能減少繁瑣的 Array.prototype.concatObject.assignfor...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)**上。若傳入 nullundefined 或非迭代物件,會直接拋出 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.isArraytypeof 檢查。
2️⃣ 淺層拷貝的誤解 以為 ...obj 可以深拷貝巢狀物件。 若需要深拷貝,使用 structuredClone(ES2021)或自行實作遞迴拷貝。
3️⃣ 展開順序不當 合併物件或陣列時,順序決定了最終結果。 明確寫出「預設 → 使用者設定」的順序,或使用 Object.assign 以同樣概念。
4️⃣ 在函式呼叫時忘記展開 fn(argsArray) 只會把陣列當作單一參數。 必須寫成 fn(...argsArray) 才會把每個元素當作獨立參數。
5️⃣ 展開大量資料造成效能問題 大型陣列或物件在展開時會產生臨時副本,佔用記憶體。 只在需要時使用,對於巨量資料考慮使用迭代器或 for...of

最佳實踐小結

  1. 保持不可變(immutability):使用展開運算子產生新陣列/物件,避免直接改變原始資料。
  2. 明確命名:如 const newState = {...oldState, updatedProp},讓程式碼可讀性提升。
  3. 結合其他語法Array.from, Object.entries, Set 等,都可以與展開運算子搭配,寫出更簡潔的資料轉換流程。
  4. 注意淺層拷貝:若涉及巢狀結構,務必在文件或註解中說明拷貝層級,防止意外的副作用。

實際應用場景

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 的展開會錯、淺層拷貝的限制、展開順序的影響等。
  • 最佳實踐:保持資料不可變、明確命名、配合其他迭代工具、留意效能。

掌握展開運算子後,你將能在 寫程式的速度、可讀性與維護成本 上獲得顯著提升。建議在日常開發中有意識地替換掉冗長的 concatObject.assign 或手寫迴圈,讓程式碼更貼近 「資料即語意」 的設計哲學。祝你寫程式愉快,持續在 JavaScript 的世界裡探索更高效的寫法! 🚀