本文 AI 產出,尚未審核

JavaScript 變數與資料型別

單元:變數與資料型別(Variables & Data Types)

主題:資料型別—Primitive vs Reference


簡介

在 JavaScript 中,資料型別是程式運作的基礎。了解什麼是 原始型別(Primitive),以及什麼是 參考型別(Reference),能讓你在寫程式時避免許多難以偵測的錯誤。
原始型別的值是不可變且直接儲存在變數本身;相對地,參考型別的值則是指向一段記憶體位置,多個變數可能共享同一個物件。這兩種行為的差異,直接影響到函式參數傳遞、陣列或物件的操作、以及效能優化。

本篇文章將以 淺顯易懂 的方式說明兩者的特性,提供實用範例、常見陷阱與最佳實踐,並探討在真實專案中如何善用這些概念。


核心概念

1. 原始型別(Primitive Types)

JavaScript 的原始型別包括:NumberStringBooleanBigIntSymbolundefinednull。這些型別的值不可變(immutable),每次對它們的「變更」其實都是產生一個新值。

範例 1:基本的原始型別賦值與複製

let a = 10;          // Number
let b = a;           // 複製值,b 也變成 10

a = 20;              // 改變 a 的值
console.log(b); // 10  // b 不受影響,因為它持有的是原本的值

重點ba 之間沒有任何連結,b 只是一個獨立的副本。

範例 2:字串的不可變性

let str1 = "Hello";
let str2 = str1;     // 複製字串

str1 = str1 + " World";
console.log(str1); // "Hello World"
console.log(str2); // "Hello"  // str2 沒被改變

即使看起來像是「改變」了 str1,實際上是產生了一個全新的字串,str2 仍指向舊的那個字串。

2. 參考型別(Reference Types)

參考型別主要是 Object(包括普通物件、陣列、函式、日期等)。變數儲存的是指向實際資料的記憶體位址,因此多個變數可以指向同一個物件。

範例 3:陣列的共享與修改

let arr1 = [1, 2, 3];
let arr2 = arr1;      // 兩個變數指向同一個陣列

arr1.push(4);         // 改變陣列內容
console.log(arr2);   // [1, 2, 3, 4]  // arr2 也被改變

此時 arr1arr2 共享同一個記憶體位置,任何一方的變更都會同步顯現在另一方。

範例 4:深淺拷貝的差異

let obj1 = { name: "Alice", address: { city: "Taipei" } };
let shallowCopy = { ...obj1 };   // 浅拷贝,僅複製第一層

shallowCopy.name = "Bob";
shallowCopy.address.city = "Kaohsiung";

console.log(obj1.name);          // "Alice"  (不受影響)
console.log(obj1.address.city); // "Kaohsiung"  (被改變)

使用展開運算子 ... 只會淺層複製第一層屬性,內部的物件仍是同一個參考。若要完全斷開關聯,需要深拷貝(例如 structuredCloneJSON.parse(JSON.stringify(...)))。

範例 5:函式參數傳遞的行為

function modifyPrimitive(val) {
  val = 100;               // 只改變局部變數
}
function modifyReference(obj) {
  obj.prop = "changed";    // 直接改變傳入的物件
}

let num = 5;
modifyPrimitive(num);
console.log(num); // 5

let data = { prop: "original" };
modifyReference(data);
console.log(data.prop); // "changed"
  • 原始型別以值傳遞(pass‑by‑value),函式內部的改變不會影響外部變數。
  • 參考型別以引用傳遞(pass‑by‑reference),函式內部的改變會直接作用於原始物件。

常見陷阱與最佳實踐

陷阱 說明 解決方案
誤以為 = 會「複製」物件 直接賦值會產生共享引用,導致不預期的資料變動。 使用 淺拷貝{...obj}Array.from)或 深拷貝structuredClonelodash.cloneDeep)。
在迴圈中直接改變傳入的陣列/物件 會把原始資料「污染」掉,影響後續程式邏輯。 建立副本 再操作,或使用 不可變資料結構(Immutable.js、Immer)。
忘記 nullundefined 的差異 null 是一個 有意 的空值,undefined 表示「未定義」或「未賦值」。 在檢查時使用 ===,或使用 可選鏈 (obj?.prop) 防止拋錯。
在比較兩個物件時使用 === 兩個不同的物件即使內容相同,=== 仍回傳 false 使用 深度相等比較_.isEqualJSON.stringify)或自行實作比較函式。

最佳實踐

  1. 保持資料的不可變性:在 React、Vue 等框架中,建議使用不可變的資料流,避免直接改變狀態。
  2. 明確區分「值」與「參考」:在函式 API 設計時,文件中說明參數是「傳值」或「傳參考」會降低使用者的誤解。
  3. 使用 const 宣告常用的物件:即使 const 只保證變數指向不會改變,仍能提醒開發者不要重新指派
  4. 適時使用 Object.freeze:在開發階段凍結物件,可快速捕捉到不小心的寫入行為。

實際應用場景

1. 表單資料的深拷貝

在單頁應用(SPA)中,使用者編輯表單時常需要「暫存」原始資料,以便「取消」時還原。此時必須 深拷貝 原始物件,避免在編輯過程中直接改變來源。

const original = {
  name: "Jane",
  address: { city: "Taichung", zip: "407" }
};

// 深拷貝(現代瀏覽器支援)
const draft = structuredClone(original);

// 使用者編輯 draft...
draft.address.city = "Tainan";

// 若使用者按下「取消」:
Object.assign(original, original); // 仍保持原始值

2. Redux 狀態管理的不可變更新

Redux 要求 state 必須是不可變的。每一次更新都必須回傳一個 新物件,而不是直接改變舊的 state。

function reducer(state = { count: 0, items: [] }, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // 產生新物件
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] };
    default:
      return state;
  }
}

3. 多執行緒(Web Worker)間傳遞資料

postMessage深拷貝(structured clone)資料,若傳遞大物件會產生效能負擔。此時可考慮傳遞 ArrayBuffer(共享記憶體)或使用 Transferable 物件,以避免不必要的複製。


總結

  • 原始型別(Number、String、Boolean、等)是 不可變 的值,賦值時會產生獨立副本
  • 參考型別(Object、Array、Function)保存的是記憶體位址,多個變數可能指向同一塊資料,改動會相互影響。
  • 正確認識「值」與「引用」的差異,可避免 資料污染、不可預期的錯誤,並提升程式的可讀性與維護性。
  • 在日常開發中,深拷貝、淺拷貝、不可變資料是處理參考型別的關鍵技巧;而在函式參數傳遞、狀態管理、跨執行緒通訊等情境下,了解兩者行為更是必備能力。

掌握了這些概念,你就能在 JavaScript 的世界裡寫出更安全高效易維護的程式碼。祝你在程式之路上越走越遠!