JavaScript 變數與資料型別
單元:變數與資料型別(Variables & Data Types)
主題:資料型別—Primitive vs Reference
簡介
在 JavaScript 中,資料型別是程式運作的基礎。了解什麼是 原始型別(Primitive),以及什麼是 參考型別(Reference),能讓你在寫程式時避免許多難以偵測的錯誤。
原始型別的值是不可變且直接儲存在變數本身;相對地,參考型別的值則是指向一段記憶體位置,多個變數可能共享同一個物件。這兩種行為的差異,直接影響到函式參數傳遞、陣列或物件的操作、以及效能優化。
本篇文章將以 淺顯易懂 的方式說明兩者的特性,提供實用範例、常見陷阱與最佳實踐,並探討在真實專案中如何善用這些概念。
核心概念
1. 原始型別(Primitive Types)
JavaScript 的原始型別包括:Number、String、Boolean、BigInt、Symbol、undefined、null。這些型別的值不可變(immutable),每次對它們的「變更」其實都是產生一個新值。
範例 1:基本的原始型別賦值與複製
let a = 10; // Number
let b = a; // 複製值,b 也變成 10
a = 20; // 改變 a 的值
console.log(b); // 10 // b 不受影響,因為它持有的是原本的值
重點:
b與a之間沒有任何連結,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 也被改變
此時 arr1 與 arr2 共享同一個記憶體位置,任何一方的變更都會同步顯現在另一方。
範例 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" (被改變)
使用展開運算子 ... 只會淺層複製第一層屬性,內部的物件仍是同一個參考。若要完全斷開關聯,需要深拷貝(例如 structuredClone 或 JSON.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)或 深拷貝(structuredClone、lodash.cloneDeep)。 |
| 在迴圈中直接改變傳入的陣列/物件 | 會把原始資料「污染」掉,影響後續程式邏輯。 | 先 建立副本 再操作,或使用 不可變資料結構(Immutable.js、Immer)。 |
忘記 null 與 undefined 的差異 |
null 是一個 有意 的空值,undefined 表示「未定義」或「未賦值」。 |
在檢查時使用 ===,或使用 可選鏈 (obj?.prop) 防止拋錯。 |
在比較兩個物件時使用 === |
兩個不同的物件即使內容相同,=== 仍回傳 false。 |
使用 深度相等比較(_.isEqual、JSON.stringify)或自行實作比較函式。 |
最佳實踐
- 保持資料的不可變性:在 React、Vue 等框架中,建議使用不可變的資料流,避免直接改變狀態。
- 明確區分「值」與「參考」:在函式 API 設計時,文件中說明參數是「傳值」或「傳參考」會降低使用者的誤解。
- 使用
const宣告常用的物件:即使const只保證變數指向不會改變,仍能提醒開發者不要重新指派。 - 適時使用
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 的世界裡寫出更安全、高效、易維護的程式碼。祝你在程式之路上越走越遠!