Pio's

探討 JavaScript 變數傳遞方式:pass-by-value

前陣子在參考試題時,無意間看到 stack overflow 上的一則問題 Is JavaScript a pass-by-reference or pass-by-value language? 可以看得出這則問題不少人回答與留言討論,讓我想深入研究這個問題,JavaScript 在呼叫 function 時傳入的是 value 還是 reference?也同時釐清什麼是 pass-by-value 跟 pass-by-reference,什麼又是 pass-by-sharing?1

探討這個問題前我們需要先簡單了解 JavaScript 的資料型態。

JavaScript 的資料型態

在 JavaScript 中資料型態分為兩種 primitive type 跟 object。

  • Primitive type:包含 number、bigint、string、boolean、null、undefined 和 symbol

  • Object:可視為 property 的集合。記憶體中儲存的是 reference,再由 reference 指向實際的 object。

接下來,來看看這兩種資料型態在重新賦值時會有什麼差異。

重新賦值

當變數的 value 為 primitive type

let a = 5;
let b = a;
b = 10;

console.log(a); // 5
console.log(b); // 10

當把值為 primitive type 的變數賦值給另一個變數後,等同於複製了 value,重新賦值其中一個變數不會連帶改變另一個變數。

variablelocationvalue
a0x015
b0x025 -> 10 (reassign)

上面可以看到 ab 對應到不同的記憶體位置,重新賦值不會相互影響。

當變數的 value 為 object

根據不同操作則有以下兩種不同結果。

  1. 先看看如果是對 object 內 property 進行賦值:
let c = {};
let d = c;
d.item = 5;

console.log(c); // {item: 5}
console.log(d); // {item: 5}

會看到 log 出來 cd 是一樣的,這是因為 let d = c 時並不是複製一份完整的 object 而是複製這個 object 的 reference value。

由下表可更明確看出在賦值時複製的內容:

variablelocationvalue (reference)
c0x010x00A888
d0x020x00A888

cd 指向相同的 object。

referencevalue
0x00A888empty -> item: 5 (reassign)

透過其中一個變數改變 object,等同於對同一個 object 進行操作。

  1. 如果將 d 重新賦值為另一個 object:
let c = {};
let d = c;
d = { item: 5 };

console.log(c); // {}
console.log(d); // { item: 5 }

原本的 c 則不會受到影響。

variablelocationvalue (reference)
c0x010x00A888
d0x020x00A888 -> 0x00B888 (reassign)
referencevalue
0x00A888empty
0x00B888item: 5

可以看到 d 因為重新賦值有不同的 reference value,cd 兩者指向不同 object。

將變數傳入 function

將變數傳入 function 與重新賦值的情況其實相當類似。

function changeValue(x) {
  x = 10;
}
let a = 5;
changeValue(a);

console.log(a); // 5

可以理解成在呼叫 function 時,引數 x 被賦值為 a

variablelocationvalue
a0x015
x0x025 -> 10 (reassign)

結果會與賦值為 primitive type 相同, x 不會影響到 a 的 value。

pass-by-value

由上述可知在呼叫 function 時傳入的是 value,pass-by-value (call-by-value) 描述的就是這種行為,在 function 內改變引數的值也不會改變原本的變數。

pass-by-sharing

那你可能會想在賦值為 object 不就可以改變原本的變數嗎?那這樣還是 pass-by-value 嗎?

function changeItem(x) {
  x.item = 'changed';
}
let c = { item: 'unchanged' };
changeItem(c);

console.log(c); // {item: 'changed'}

透過引數 x 更改 object 的 property,c 的 value 也改變了。

variablelocationvalue (reference)
c0x010x00A888
x0x020x00A888

由記憶體位址可看出兩者為相同的 reference value。

referencevalue
0x00A888item: 'unchanged' -> item: 'changed' (reassign)

同一個 reference value 對應到相同的 object。

還記得我們剛剛說賦值為 object 的時候,其實是複製 object 的 reference。而在呼叫 function 時傳入 object,其實是傳入一份複製的 reference value 進去,只是我們可以透過 reference 修改 object 所以才會造成原本的變數連帶改變。

為了顯示跟 pass-by-value 的差異,另一個名詞 pass-by-sharing (call-by-sharing) 被用來形容這個行為,但就底層而言依舊是 pass-by-value,只是傳進去從 primitive value 變成了 reference value。

那什麼才是 pass-by-reference 呢?

注意這邊的 reference 與前面講的 reference 兩者意義不同,為了能夠區別建議稱為 alias 較為恰當。

稱為 alias 是因為在 pass-by-reference 語言裡呼叫 function 傳入的變數不會有 pass-by-value 的複製行為,function 內的引數相當變數本身。以記憶體來說明 pass-by-reference 的話,function 內的引數 x 與變數 a 會是相同的記憶體位置。

variablelocation
a0x01
x0x01

所以對引數做任何操作(包括重新賦值或修改 property)都是等同作用在變數上。所以不管傳進去的 primitive types 或是 object 都是可以藉由重新賦值引數改變原來的變數。

而在 JavaScript 中沒有這個行為,也無法利用下面會提到的 pointer 模擬類似的效果。

pass-by-address

在不支援 pass-by-reference 的語言裡,如 C、ML、Rust,可以藉由傳入 pointer (指標)來模擬 pass-by-reference 的行為,這邊用模擬這個詞是因為就算是 pointer,在 function 內依舊有複製行為(複製的對象是 point),以底層來看依然是 pass-by-value 的模式,但同樣為了做出區別,也可稱為 pass-by-address 或 pass-by-pointer。

結論

  • JavaScript 呼叫 function 傳入的都是 value。
  • JavaScript 是 pass-by-value 語言,更細分也可以說是 pass-by-sharing。
  • JavaScript 沒有 pass-by-reference 的行為也無法模擬這個行為。

參考資料

  1. Evaluation strategy

  2. 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

  3. JavaScript's Memory Management Explained

Footnotes

  1. pass-by-value、pass-by-reference、pass-by-sharing 也可以稱為 call-by-value、call-by-reference、call-by-sharing