JavaScript 函式(Functions)
傳值 vs. 傳參考
簡介
在 JavaScript 中,函式的參數傳遞方式是每位開發者在撰寫程式時必須先弄清楚的基礎概念。傳值(Pass‑by‑Value) 與 傳參考(Pass‑by‑Reference)(更精確地說是「傳共享」)直接影響變數在函式內部被修改時,原始資料是否會同步改變。
如果不了解這兩者的差異,常會發生「資料被不小心改寫」或「期待的結果沒有出來」的情況,尤其在處理陣列、物件或大型資料結構時,問題更容易隱蔽。本文將以 淺顯易懂 的方式說明 JavaScript 的傳值與傳參考機制,提供實作範例、常見陷阱與最佳實踐,最後帶出在真實專案中的應用情境,幫助初學者到中級開發者快速上手與避免踩雷。
核心概念
1. 基本類型 vs. 參考型別
| 類型 | 例子 | 傳遞方式 |
|---|---|---|
| 原始值(Primitive) Number、String、Boolean、Symbol、BigInt、undefined、null |
let a = 10;、let s = "hello" |
傳值:函式收到的是值的副本,原本變數不會被影響。 |
| 參考值(Reference) Object、Array、Function |
let obj = {x:1};、let arr = [1,2,3] |
傳參考(共享):函式收到的是指向同一記憶體位置的參考,若在函式內修改 屬性,外部也會同步變化。 |
注意:JavaScript 永遠是「傳值」語言,參考型別的值本身也是傳值(即傳遞的是「參考」的副本)。因此常說「傳參考」其實是「傳共享的參考副本」。
2. 為什麼會這樣?
- 原始值佔用的記憶體較小,直接複製內容即可。
- 物件、陣列等結構可能非常龐大,若每次呼叫函式都完整複製,效能會大幅下降。
JavaScript 採用 指標(reference) 方式,只傳遞「指向同一塊記憶體」的參考。
3. 變更的層級
| 變更層級 | 會影響外部變數嗎? |
|---|---|
重新指派(param = newValue;) |
不會,因為僅改變了函式內的參考副本。 |
修改屬性(param.prop = ...;) |
會,因為兩個參考指向同一物件。 |
陣列方法(push/pop/...) |
會,同樣是修改同一記憶體內容。 |
程式碼範例
以下示範 5 個常見情境,說明傳值與傳參考的行為差異。每段程式碼都加上中文註解,方便閱讀。
範例 1:原始值(傳值)
function addOne(num) {
// 參數 num 是傳值的副本
num = num + 1;
return num;
}
let a = 5;
let result = addOne(a);
console.log(a); // 5 ← 原本的 a 沒變
console.log(result); // 6
重點:
a的值在函式外部保持不變,因為num只是一個 副本。
範例 2:物件屬性修改(傳參考)
function setName(person, newName) {
// person 是指向同一物件的參考
person.name = newName; // 直接改變物件屬性
}
let user = { name: "Alice", age: 25 };
setName(user, "Bob");
console.log(user.name); // "Bob" ← 物件內容被修改
重點:即使
person只是一個參考的副本,只要改的是 屬性,外部的user也會同步變化。
範例 3:重新指派參考(不會影響外部)
function replaceArray(arr) {
// 重新指派 arr 為全新陣列
arr = [4, 5, 6];
console.log("inside:", arr); // [4,5,6]
}
let numbers = [1, 2, 3];
replaceArray(numbers);
console.log("outside:", numbers); // [1,2,3] ← 沒被改變
重點:
arr在函式內被賦值為新陣列,只改變了 參考的副本,不會影響外部的numbers。
範例 4:陣列內容變更(會影響外部)
function pushItem(arr, item) {
// 直接對陣列執行 push,改變底層記憶體
arr.push(item);
}
let list = [10, 20];
pushItem(list, 30);
console.log(list); // [10,20,30] ← 內容被修改
重點:
push、pop、splice等會直接改變陣列本身的操作,會在外部可見。
範例 5:使用 Spread / Object.assign 防止意外修改
function updateUser(original, updates) {
// 使用展開運算子建立新物件,保護原始資料
const newUser = { ...original, ...updates };
return newUser;
}
let profile = { name: "Charlie", age: 28 };
let updated = updateUser(profile, { age: 29 });
console.log(profile); // { name: "Charlie", age: 28 } ← 未被改動
console.log(updated); // { name: "Charlie", age: 29 }
技巧:當不想讓函式改變傳入的參考型別時,先複製(淺層複製或深層複製)再操作,是最常見的防呆手法。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 / 最佳實踐 |
|---|---|---|
| 誤以為參考會被重新指派 | 原始物件仍被改動,導致不可預期的 bug | 若要保護原始物件,使用 展開運算子、Object.assign 或 JSON.parse(JSON.stringify(...)) 進行深拷貝。 |
| 在迴圈或遞迴中直接修改參數 | 可能意外改變整個資料結構,難以追蹤 | 使用不可變資料(immutable)概念,或在每次遞迴前先 clone。 |
混用 == 與 === 比較參考 |
== 會把參考轉為原始值,導致偽相等 |
永遠使用 ===,比較參考時直接比對記憶體位址。 |
| 忘記函式內的副作用 | 函式看似「純函式」但實際改變外部狀態 | 在函式文件註明 是否有副作用,或盡量寫 純函式(不改變外部參數)。 |
| 深層物件的淺拷貝 | 只拷貝第一層,內層仍共享,仍會被改寫 | 需要 深拷貝 時,可使用 structuredClone(Node 17+ / 瀏覽器支援)或自行遞迴拷貝。 |
最佳實踐摘要
- 預設不變:除非真的需要改變傳入的參考,否則先 clone 再操作。
- 明確命名:若函式會改變參數,使用
mutateXxx、updateXxx等動詞提醒使用者。 - 純函式優先:純函式(不產生副作用)更易測試、除錯與重構。
- 使用
const宣告參考:const obj = {}只能保證「參考不會被重新指派」,不保證內容不可變,仍需配合Object.freeze或 immutable 套件。
實際應用場景
1. 表單資料的即時編輯
在單頁應用(SPA)中,使用者填寫表單時常會把表單資料存於 state(例如 React 的 useState)。若直接把 state 物件傳給子元件,子元件若在內部直接修改屬性,父層的 state 會同步被改寫,導致 UI 更新不一致。解法是:
// 父層
const [form, setForm] = useState({ name: "", email: "" });
function handleChange(updated) {
// 用展開運算子產生新物件,保持不變性
setForm(prev => ({ ...prev, ...updated }));
}
2. 資料庫批次更新
在後端 Node.js 程式中,常會先從資料庫撈出多筆記錄(陣列),然後對每筆資料進行計算後回寫。若在迴圈內直接 push 到原始陣列,會改變迭代過程,可能導致「跳過」或「重複」的情況。安全做法:
const rows = await db.query("SELECT * FROM orders");
const updatedRows = rows.map(row => ({
...row,
total: row.price * row.quantity
}));
// 此時 rows 本身未被改動,updatedRows 為全新陣列
await db.bulkUpdate(updatedRows);
3. 函式庫的 API 設計
設計一個工具函式 mergeOptions(defaults, overrides) 時,若直接在 defaults 上做 Object.assign(defaults, overrides),使用者呼叫後會發現自己的預設設定被意外改寫。正確做法:
function mergeOptions(defaults, overrides) {
// 產生新物件,保護傳入的參數
return Object.assign({}, defaults, overrides);
}
總結
- 原始值(Number、String、Boolean…)在函式呼叫時是 傳值,函式內部的變更不會影響外部。
- 參考型別(Object、Array、Function)則是 傳參考(共享):函式接收到的是指向同一記憶體位置的參考副本,修改屬性或內容會同步變化,但重新指派參考本身不會影響外部。
- 為避免不必要的副作用,在需要保護原始資料時,先做淺層或深層複製(Spread、
Object.assign、structuredClone…)。 - 在實務開發中,保持資料不變性、明確標示副作用、以及 使用純函式,都是提升程式碼可讀性、可測試性與維護性的關鍵。
掌握了傳值與傳參考的差異後,你將能更自信地設計 API、處理大型資料結構,並寫出更安全、更易維護的 JavaScript 程式。祝你在開發旅程中玩得開心,寫出乾淨、可靠的程式碼!