JavaScript 是「傳值」或「傳址」
JavaScript 基本型別 (Primitives) 內的資料,會是以純值的形式存在 ( String
、Number
、Boolean
、Null
以及 Undefined
),而物件型別 (Object) 指的是可能由零或多種不同型別 (包括純值與物件) 所組合成的物件。
基本型別
當我們今天要給變數資料的時候,假設我們給兩個變數分別設定為 10
:
var a = 10;
var b = 10;
// 在 JavaScript 判斷是否「相等」用 ===
console.log( a === b ); // true
在基本型別的時候,會認為這兩個變數的「值」是相等的。 這應該不難理解,因為兩個變數的數值都是 10
。
同樣地,在字串的情況下也是:
var a = 'Kuro';
var b = 'Kuro';
var c = 'Jack';
console.log( a === b ); // true
console.log( a === c ); // false
所以在基本型別,當我們判斷這兩個變數是否相等,看的是裡面的內容,也就是「值」。
物件型別
在物件型別的狀況下就不同了。
這裡我們分別宣告兩個物件,也都有個 value
的屬性。
var obj1 = { value: 10 };
var obj2 = { value: 10 };
猜猜看, obj1 === obj2
的結果會是?
.
.
.
.
.
.
答案會是 false
。 想當然如果是 true 我就不用另外寫這篇了
剛接觸 JavaScript 的朋友可能無法理解這點,沒關係,我們繼續往下看。
在 JavaScript 的物件,我們可以把它看作是一個「實體 (instance)」,什麼意思呢,這裡我舉個例子。
假設我口袋裡有十塊錢,你口袋裡也有十塊錢。 這樣我們就有二十塊錢(不是
那麼在正常情況下,我們各自的十塊錢 可以買到的東西應該是一樣多的 對吧? 這個時候,我可以說我們各自的十塊錢是「等值」的。 用程式碼來說,就像這樣:
var a = 10;
var b = 10;
console.log( a === b ); // true
那麼在「物件」的情況下呢? 剛剛說 JavaScript 的物件都應該看作是一個「實體」。
以「實體」的前提下,假設我在我口袋裡的十塊錢用麥克筆上面打個 X
,除非我是劉謙,此時你口袋的十塊錢應該是不可能有 X
的記號對吧?
// 兩個 coin 的價值都是 10,但 coin1 與 coin2 卻不是同一個實體。
var coin1 = { value: 10 };
var coin2 = { value: 10 };
console.log( coin1 === coin2 ); // false
// 我在 coin1 畫了一個 X
coin1.cross = true;
// coin2.cross 當然不可能會有東西
console.log( coin2.cross ); // undefined
當然 JavaScript 的物件沒這麼單純,這裡暫時用極簡化的例子幫助各位理解。
變數的更新與傳遞
既然大家都知道,「變數」裡面的內容是可以被變動,那麼在理解了「基本型別」與「物件型別」在比較時的不同後,接著就來聊聊變數的更新與傳遞,這部分我們一樣分成「基本型別」與「物件型別」兩種來看。
基本型別的更新與傳遞
還記得十塊錢的範例嗎,如同稍早所說,在基本型別的變數中,我們看的是變數裡頭的「值」。 換言之,我們在複製變數的時候,複製的也是那個變數的「值」:
var a = 10;
var b = a;
console.log( a ); // 10
console.log( b ); // 10
可以看到,變數 b
的值是透過複製變數 a
的值而來。
但並不代表當變數 a
更新之後,會去影響變數 b
的數值:
a = 100;
// 變數 b 依然是 10,而變數 a 變成了 100
console.log( a ); // 100
console.log( b ); // 10
簡單來說, var b = a;
表面上看起來變數 b
的內容是透過複製變數 a
而來,但此時若變數 a
的內容為基本型別時,實際上變數 b
是去建立了一個新的值,然後將變數 a
的內容複製了一份過來。
這時候 a
與 b
各自是獨立的。
所以當變數 a
的內容後來經過更新變成 100
之後,變數 b
的內容依舊保持原來的 10
而不受影響。
像這種情況,我們通常會稱作「傳值」 (pass by value)。
物件型別的更新與傳遞
那麼換成了物件型別呢? 讓我們回到剛剛 coin 的例子,並且稍微修改一下:
var coin1 = { value: 10 };
var coin2 = coin1;
console.log( coin1.value ); // 10
console.log( coin2.value ); // 10
乍看之下與前面基本型別 (純值) 的情況沒什麼不同,但是:
coin1.value = 100;
console.log( coin1.value ); // 100
console.log( coin2.value ); // 100
當 coin1.value
的內容被更新了之後,連帶著 coin2.value
卻也跟著更新了。
而且此時,你透過 ===
去檢查兩者實體時,會發現 coin1
與 coin2
實際上是同一個實體!
console.log( coin1 === coin2 ); // true
聰明的你應該已經猜到,其實「物件」這類資料型態,在 JavaScript 中是透過「引用」的方式傳遞資料的。
什麼意思? 這裡我用兩張圖來表示:
var coin1 = { value: 10 };
首先我們建立起一個新的物件的時候,JavaScript 會在記憶體的某處建立起一個物件 (圖右側),然後再將這個 coin1
變數指向新生成的物件。
var coin2 = coin1;
接著,當我們宣告了第二個變數 coin2
之後,並且透過 =
將 coin2
指向 coin1
的位置。 於是我們更新了 coin1.value
的內容後, coin2.value
的內容也理所當然地被更新了。
coin1.value = 100;
console.log( coin1.value ); // 100
console.log( coin2.value ); // 100
所以實際上可以看出,coin1
與 coin2
這兩個變數是指向同一個實體的。
像這種透過引用的方式來傳遞資料,接收的其實是引用的「參考」而不是值的副本時, 我們通常會稱作「傳址」 (pass by reference)。
JavaScript 是「傳值」或「傳址」?
回到主題,所以我說那個 JavaScript 是「傳值」或「傳址」呢?
在大多數的情況下,基本型別是「傳值」,而物件型別會是「傳址」的方式,但凡事都有例外。
我們來看看下面這個例子:
var coin1 = { value: 10 };
function changeValue(obj) {
obj = { value: 123 };
}
changeValue(coin1);
console.log(coin1); // ?
猜猜看,經過 changeValue(coin1)
操作後的 coin1
會是什麼?
答案仍是 { value: 10 }
。
剛剛說過,物件型別會是「傳址」的方式來更新資料,那應該會是 { value: 123 }
才對,為什麼依然不變?
事實上,JavaScript 不屬於單純的傳值或傳址。 更準確一點來說,JavaScript 應該屬於透過 pass by sharing (還沒找到合適的中文翻譯) 來傳遞資料。
「傳值」或「傳址」對大多數的開發者來說應該都不陌生,那麼「pass by sharing」又是什麼呢?
Pass by sharing
「Pass by sharing」的特點在於,當 function
的參數,如 function changeValue(obj){ ... }
中的 obj
被重新賦值的時候,外部變數的內容是不會被影響的。
var coin1 = { value: 10 };
function changeValue(obj) {
obj = { value: 123 };
}
changeValue(coin1);
console.log(coin1); // 此時 coin1 仍是 { value: 10 }
如果不是重新賦值的情況,則又會回到大家所熟悉的狀況:
var coin1 = { value: 10 };
function changeValue(obj) {
// 僅更新 obj.value,並未重新賦值
obj.value = 123;
}
changeValue(coin1);
console.log(coin1); // 此時 coin1 則會變成 { value: 123 }
結論
所以 JavaScript 到底屬於何種策略? 我認為 JavaScript 應該更屬於 Pass by sharing 的形式。
參考 ECMA-262-3 in detail. Chapter 8. Evaluation strategy 所說:
Regardless of usage concept of reference in this case, this strategy should not be confused with the “call by reference” discussed above. The value of the argument is not a direct alias, but the copy of the address.
由於在 JavaScript 的物件類型是可變的 (mutable),當物件更新時,會影響到所有引用這個物件的變數與其副本,修改時會變動到原本的參考,但當賦與新值時,會產生新的實體參考。
而基本型別則是不可變的 (immutable),當你更新了某個基本型別的值時,與那個值的副本完全無關:
var a = 10;
var b = a;
a = 100;
console.log(a); // 100
console.log(b); // 10
這個時候在基本型別的操作下,以 Pass by sharing 的行為來說,與 Pass by value 的結果是完全一樣的,修改時永遠只能賦與新值。