
從比較運算子來釐清基本觀念
在正式進入這個主題之前,我們先來複習一下 JavaScript 的基本觀念。首先是「比較運算子」:
|
|
上面這題印出的答案是甚麼呢?沒錯,是 true
。
看到這邊,你可能會想翻桌,這麼簡單的問題還要問嗎?值與型別都相同,用嚴格相等比較運算子(===),結果當然是 true
啊。
先壓壓怒氣,再來看看下面這題:
|
|
「廢話,當然也是 true
啊!」
你確定嗎?
那麼下面這幾題呢?
|
|
執行以上五個判斷式的結果是什麼呢?
結果竟然全部是 false
!
原因就在於 Javascript 中不同型別的變數會有不同的儲存和傳遞資料的方式,大致上可以分為 「傳值」(call/pass by value) 和 「傳址(也有譯為「傳參考」)」(call/pass by reference) 兩種。
參數的傳遞方式
以傳統的說法來說,若變數指派的值為基本型別(number、string、boolean、null、undefined、symbol),那麼賦值是「傳值」。
若變數指派的值為物件型別(object、array、function),那麼賦值是「傳址」。
型別 | 參數傳遞方式 | 英譯 |
---|---|---|
基本型別(number、string、boolean、null、undefined、symbol) | 傳值 | by value |
物件型別(object、array、function) | 傳址/傳參考 | by reference |
但為什麼說上面這個說法是「傳統說法」呢?
因為也有人認為 Javascript 既不是傳值也不是傳址,而是「有時看起來像傳值,有時看起來像傳址」, 實際上應該說是「傳共享(pass/call by sharing)」 。
但也有人認為 Javascript 沒有所謂的「傳址」或「傳共享」,實際上就只有「傳值」(call/pass by value)。
這眾說紛紜的狀況是怎麼回事呢?要釐清這道經典的爭議題,就讓我們先從傳統的說法來理解何謂「傳值」?何謂「傳址」?
何謂「傳值(call/pass by value)」?
對電腦來說,實際儲存和呼叫資料是以「記憶體位址」來運作,而宣告變數,就是來幫助我們以別名來與實際的資料儲存位址作連結。變數會指向記憶體位置來調用資料進行運算。
|
|
比如說,當我們宣告一個變數 a
,並賦值一個資料型別為數字的值 3
的時候,實際上在電腦中是這樣運作的:

將 3
這個值儲存在 0x01
這個記憶體位置,然後把變數 a
指向這個記憶體位置。
如果此時宣告一個新變數 b
,並把變數 a
的值賦值給變數 b
:
|
|
那麼記憶體位置 0x01
中儲存的值 3
就會被複製一份到 0x02
,並將新變數 b
指向記憶體位置 0x02
。(如下圖)

這就是所謂的「傳值」。
也就是當 變數指派的值是基本型別 時,變數間做 =
賦值時,傳遞的是變數的值。
此時,不同變數雖然值相同,但依舊是儲存在不同的記憶體位址。
若這時對其中一個變數再重新賦值的話,另一個變數的值並不會因此而更動:
|
|

當變數的值為基本型別時,若對該變數重新賦值的話,並不是直接修改值的資料內容,而是在記憶體內部重新分配一個區塊存放新的值,變數就指向新分配的記憶體區塊,原本舊的記憶體區塊還是放著舊值,所以舊值其實是不可變動的(immutable)。有 by value 特性的資料型別大多是不可變動的(immutable)。
何謂「傳址(call/pass by reference)」?
若變數指派的值為物件型別(object、array、function),那麼賦值就是「傳址」。
這又是甚麼意思呢?
因為物件型別儲存的是物件中的每個屬性(property),一個屬性包含一組「鍵」(key)和「值」(value),一組「鍵值對(Key-Value Pairs)」稱做物件的一個屬性。
這組「鍵值對」儲存的方式是:儲存屬性「鍵(key)」的值 以及 儲存屬性「值(value)」的記憶體位置。(觀念來源:JavaScript - 參數傳遞方式 (2))
我將我對這段話的理解,繪製成下圖:

也就是變數 obj
指向的記憶體位置 0x03
儲存了一個物件型別的資料,該物件有兩個屬性,第一個屬性的「鍵」 key0
的「值」是記憶體位置 0x01
,而 0x01
儲存了一個字串型別的資料 "aa"
。第二個屬性的「鍵」 key1
的「值」是記憶體位置 0x02
,而 0x02
儲存了一個字串型別的資料 "bb"
。
若此時欲將變數 obj
的值,賦值給變數 obj2
:
|
|
電腦底層的運作方式是如何呢?
剛剛處理基本型別的值,用的是傳值(call/pass by value)的方式。如果物件型別也用一樣的方法的話,那麼應該就是將記憶體位置 0x03
的物件型別資料複製一份到記憶體位置 0x04
,再將變數 obj2
指向記憶體位置 0x04
。
可是,如果我們以這個概念來操作以下程式碼的話,就會發現狀況有點不一樣:
|
|
如果變數 obj2
指向的是記憶體位置 0x04
,那麼第二次 console.log
時,印出來的結果應該會不一樣才對。但是變數 obj
和 變數 obj2
卻印出一樣的結果!
也就是說,下圖用「傳值」的方式來理解物件型別的參數傳遞是錯誤的:

正確的理解是,當變數指派的值是物件型別時,變數間做 =
賦值時,傳遞的是變數的記憶體位址,即所謂「傳址」。

這樣的話,剛剛那段程式碼的運作邏輯,就解釋得通了。
obj.key0 = "cc";
是將字串 "cc"
儲存在新的記憶體位置,再將 obj.key0
指向新的記憶體位置,但是原先儲存整個物件資料的記憶體位置並沒有變動,由於 obj
和 obj2
儲存的是同一個記憶體位置,所以 obj === obj2
的結果會是 true
。
真正變動的只有屬性的值所指向的記憶體位置。

這時我們再回過頭來看,一開始關於嚴格相等比較運算子(===)的那道題目,就可以理解答案是 false
的原因了:
|
|
因為 變數c
和 變數d
所儲存的物件資料,其值所指向的記憶體空間並不相同,因此嚴格比對下的結果會是 false
。
關於 immutable 和 mutable 的觀念
在上面的例子中,實際修改物件的值時,將會修改到的是物件的值所參考到的記憶體位置,可以發現舊值是可變動的(mutable)。
這裡就帶出了 「不可變的(immutable)」 和 「可變的(mutable)」 的兩種觀念。
當變數的值為基本型別時,若要修改值,其實是在記憶體內部重新分配一個區塊存放新的值,而變數就會指向新分配的記憶體區塊,原本舊的記憶體區塊還是放著舊值,所以舊值為 immutable。
有 by value 特性的資料型別大多是 immutable。
Object, Array, Function, Map 和 Set 等物件型別,在實際修改物件的值時,將會修改到的是物件的值所參考到的記憶體位置,所以舊值為 mutable。
有 by reference 特性的型別大多是 mutable。
關於 pass by sharing 的討論
而物件型別的「傳址」又還有一個例外情況,故又有討論認為 Javascript 的策略其實不是「傳值(call/pass by value)」,也不是「傳址(call/pass by reference)」而是「傳共享(call/pass by sharing)」。
這點就見仁見智了,此處對於 Javascript 究竟是不是該歸屬於「pass by sharing」不去細究,想進一步了解的話可再從下方的參考資料中去延伸閱讀。
此處只對這個例外情況稍作說明,這個例外情況是什麼呢?
就是當變數的值為物件型別,變數間做 =
賦值時,傳遞的是變數的記憶體位址,即「傳址」,但如果此時又再對其中一個變數用 物件實字(object literal) 的方式重新賦值,或在物件中加入新的 key 屬性的話,那麼,原本指向同一個記憶體位址的兩個變數,其中重新賦值的那個變數會指向不同的記憶體位址來儲存新的值。
|
|

前一章節探討物件型別的值,有說到物件型別的舊值為「可變動的(mutable)」,但從上面這個例子來看,以物件實字重新賦值時,舊值並非「可變動的(mutable)」,而是指向新的記憶體位置,這個狀況似乎比較像基本型別「不可變動的(immmutable)」性質以及「傳值」的運作方式。
是不是覺得之前說的「傳值」和「傳址」觀念有點開始崩裂了呢?
再來看另外一種例外狀況,是經由第三方函式來傳遞參數。
|
|
在上面的例子中,因為是「傳址」,所以物件 obj
的第一個鍵值確實被更動了。
但如果用在第三方函式中以物件實字(object literal)重新賦值呢?卻變成了「傳值」的運作模式。
|
|
在上面這個例子中,把 obj
傳入函式 changeObj(obj);
裡面,並對 obj
重新賦值。如果物件型別的值是「傳址」的話, obj
應該會被指向新的記憶體位置,且其值會變成 {key2: 'cc'}
。
但最後結果卻是 obj
並未被更動,仍維持舊值。
這樣看來,物件型別的值在參數的傳遞上,既不是 pass by value 也不是 pass by reference。
由於有這些奇怪的狀況,所以才有說法認為 Javascript 不是「傳值(by value)」,也不是「傳址(by reference)」而是「傳共享(pass by sharing)」。
意思就是說,讓 function 裡面 xyz
跟外面的 obj
「共享」同一個 object,所以可以透過 function 裡面的 xyz 修改「共享到的那個 object」的資料。
其實 Javascript 實際上就只有「傳值(by value)」
看到這個副標,你大概會想:「很好,現在又是甚麼狀況?」
會產生這些眾說紛紜的說法,原因在於大家對於 Value 和 Reference 的定義不同。
我們來回頭看看剛剛解釋「by reference」所使用的圖:

如果把記憶體位置 0x03
視作變數 obj
和 obj2
所儲存的值,那麼其實「pass by reference」也可以說是「pass by value」的一種,只是這個 value 是 reference 而已。
關於這個問題的討論相當多而且複雜,以上只是我以自己所能理解的程度做的簡單整理。而對於 Javascript 究竟是「pass by sharing」還是只有「pass by value」,就留待諸君各自判斷了。
參考資料:
- 談談 JavaScript 中 by reference 和 by value 的重要觀念
- 重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?
- JavaScript 的「傳值」與「傳址」
- 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
- 理解 mutable VS immutable 物件
- [JavaScript] Javascript中的傳值 by value 與傳址 by reference
- 林儀泰:簡單介紹JavaScript參數傳遞
- I Want To Know JS系列 第 7 篇:JavaScript - 參數傳遞方式 (1)
- I Want To Know JS系列 第 8 篇:JavaScript - 參數傳遞方式 (2)
- 你不可不知的 JavaScript 二三事#Day26:程式界的哈姆雷特 —— Pass by value, or Pass by reference?
- call by value 傳值 & call by reference 傳址