探討 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,重新賦值其中一個變數不會連帶改變另一個變數。
variable | location | value |
---|---|---|
a | 0x01 | 5 |
b | 0x02 | 5 -> 10 (reassign) |
上面可以看到 a
與 b
對應到不同的記憶體位置,重新賦值不會相互影響。
當變數的 value 為 object
根據不同操作則有以下兩種不同結果。
- 先看看如果是對 object 內 property 進行賦值:
let c = {};
let d = c;
d.item = 5;
console.log(c); // {item: 5}
console.log(d); // {item: 5}
會看到 log 出來 c
與 d
是一樣的,這是因為 let d = c
時並不是複製一份完整的 object 而是複製這個 object 的 reference value。
由下表可更明確看出在賦值時複製的內容:
variable | location | value (reference) |
---|---|---|
c | 0x01 | 0x00A888 |
d | 0x02 | 0x00A888 |
c
與 d
指向相同的 object。
reference | value |
---|---|
0x00A888 | empty -> item: 5 (reassign) |
透過其中一個變數改變 object,等同於對同一個 object 進行操作。
- 如果將
d
重新賦值為另一個 object:
let c = {};
let d = c;
d = { item: 5 };
console.log(c); // {}
console.log(d); // { item: 5 }
原本的 c
則不會受到影響。
variable | location | value (reference) |
---|---|---|
c | 0x01 | 0x00A888 |
d | 0x02 | 0x00A888 -> 0x00B888 (reassign) |
reference | value |
---|---|
0x00A888 | empty |
0x00B888 | item: 5 |
可以看到 d
因為重新賦值有不同的 reference value,c
與 d
兩者指向不同 object。
將變數傳入 function
將變數傳入 function 與重新賦值的情況其實相當類似。
function changeValue(x) {
x = 10;
}
let a = 5;
changeValue(a);
console.log(a); // 5
可以理解成在呼叫 function 時,引數 x
被賦值為 a
。
variable | location | value |
---|---|---|
a | 0x01 | 5 |
x | 0x02 | 5 -> 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 也改變了。
variable | location | value (reference) |
---|---|---|
c | 0x01 | 0x00A888 |
x | 0x02 | 0x00A888 |
由記憶體位址可看出兩者為相同的 reference value。
reference | value |
---|---|
0x00A888 | item: '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
會是相同的記憶體位置。
variable | location |
---|---|
a | 0x01 |
x | 0x01 |
所以對引數做任何操作(包括重新賦值或修改 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 的行為也無法模擬這個行為。
參考資料
Footnotes
-
pass-by-value、pass-by-reference、pass-by-sharing 也可以稱為 call-by-value、call-by-reference、call-by-sharing ↩