iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 5
7
Modern Web

重新認識 JavaScript系列 第 5

重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?

在介紹完變數與資料型別之後,本來想再繼續往運算式寫去,但我昨晚 人中之龍極二打到一半 突然想起有個很重要的部分遺漏了,那就在今天用一篇文章的篇幅來說明吧。

在前面幾天的文章當中,我們一直強調在 JavaScript 的資料可以分成「基本型別」(Primitives) 與「物件型別」(Object) 兩大類。

基本型別內的資料,會是以純值的形式存在 ( stringnumberbooleannullundefined ),而物件型別指的是可能由零或多種不同型別 (包括純值與物件) 所組合成的物件。


基本型別

當我們今天要給變數資料的時候,假設我們給兩個變數分別設定為 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 中是透過「引用」的方式傳遞資料的。

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

https://ithelp.ithome.com.tw/upload/images/20171208/200655041RlwNOp0RU.png

var coin1 = { value: 10 };

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

https://ithelp.ithome.com.tw/upload/images/20171208/200655046SEcyZbNfA.png

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 是「傳值」或「傳址」呢?

在大多數的情況下,基本型別是「傳值」,而物件型別會是「傳址」的方式,但凡事都有例外

我們來看看下面這個例子:

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 }

Pass by value、 Pass by reference 、Pass by sharing

所以 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 的結果是完全一樣的,修改時永遠只能賦與新值。


那麼以上就是今天分享的主題,感謝各位看到這裡,我要回去打電動了,明天見,掰。


上一篇
重新認識 JavaScript: Day 04 物件、陣列以及型別判斷
下一篇
重新認識 JavaScript: Day 06 運算式與運算子
系列文
重新認識 JavaScript37

2 則留言

0
fysh711426
iT邦研究生 4 級 ‧ 2017-12-09 09:55:40

感謝大大分享,原來有 call by sharing,
小弟有在網路上看到過相關文章,
內容講述,javascript 其實只有傳值呼叫,
物件的傳遞也只是參考的值的傳遞,
因為 javascript 的物件變數,本身並不像 c++ 那樣代表實體,
而是類似指標的參考,
當然這只是解釋的角度不同,都是在講同一個概念。
/images/emoticon/emoticon41.gif

0
javascript
iT邦新手 2 級 ‧ 2017-12-11 09:20:24

「傳值」或「傳址」對大多數的開發者來說應該都不陌生,那麼「psaa by sharing」又是什麼呢?

psaa → pass

已修正,感謝提醒

我要留言

立即登入留言