JavaScript 基本型別 (Primitives) 內的資料,會是以純值的形式存在 ( StringNumberBooleanNull 以及 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 的內容複製了一份過來。

這時候 ab 各自是獨立的。

所以當變數 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 卻也跟著更新了。

而且此時,你透過 === 去檢查兩者實體時,會發現 coin1coin2 實際上是同一個實體!

console.log( coin1 === coin2 );    // true

聰明的你應該已經猜到,其實「物件」這類資料型態,在 JavaScript 中是透過「引用」的方式傳遞資料的。

什麼意思? 這裡我用兩張圖來表示:

JavaScript Object 示意圖 - 1

var coin1 = { value: 10 };

首先我們建立起一個新的物件的時候,JavaScript 會在記憶體的某處建立起一個物件 (圖右側),然後再將這個 coin1 變數指向新生成的物件。

JavaScript Object 示意圖 - 2

var coin2 = coin1;

接著,當我們宣告了第二個變數 coin2 之後,並且透過 =coin2 指向 coin1 的位置。 於是我們更新了 coin1.value 的內容後, coin2.value 的內容也理所當然地被更新了。

coin1.value = 100;

console.log( coin1.value );       // 100
console.log( coin2.value );       // 100

所以實際上可以看出,coin1coin2 這兩個變數是指向同一個實體的。

像這種透過引用的方式來傳遞資料,接收的其實是引用的「參考」而不是值的副本時, 我們通常會稱作「傳址」 (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 的結果是完全一樣的,修改時永遠只能賦與新值。