[JavaScript] Javascript中的傳值 by value 與傳址 by reference

itsems
itsems_frontend
Published in
8 min readAug 30, 2020
Photo by Aditya Joshi on Unsplash

先看兩個有趣的例子:

var a = 'Hi';   // 定義 a 的內容為'Hi'字串
var b;
b = a; // 將 b 指定等於 a 字串
console.log(b); // 'Hi'
b = 'Hello'; // 再將 b 的內容更動為'Hello'
console.log(a); // 'Hi'
console.log(b); // 打印 b 出現變更後的內容'Hello'

看起來就跟想像中差不多,再來看下一個:

var aa = {age:18}// 定義 a 是內容為{age:18}的物件
var bb;
bb = aa; // 將 b 指定等於 a 物件
console.log(bb); // {age:18}
bb.age = 20; // 再將 b 的age值更動為 20
console.log(aa); // {age:20} !?!?!?
console.log(bb); // {age:20}

😲😲😲 !?!?

我改變了 bb,aa 也跟著改變了,可是剛剛不會阿 !?

在 Javascript 中,參數的傳遞分為「傳值」和「傳址」兩種方法:

  • 傳值可稱為 Call by value 或 Pass by value
  • 傳址則稱為 Call by reference 或 Pass by reference,或是有時候也會聽到的「傳參考」

基本型別處理值 by value

Javascript 中除了物件以外的所有型別,都是基本型別,物件的定義是一對 key + value,而基本型別,就只是一個純值,稱為基本型別 (Primitive type),在 Javascript 會是以傳值 by value 的方式傳遞,基本型別共有以下六種:

  • Boolean:布林
  • Null:空值
  • Undefined:未定義
  • Number:數字
  • String:字串
  • Symbol(於 ECMAScript 6 新定義):符號

變數、記憶體位置、值

在電腦底層的世界,可以想像記憶體空間就像是一個一個空間,每一個空間都有他的位址,並可以空間內儲存值。為了方便人類取用,才有了「變數」的存在,拿來連結(指向)這些記憶體位址,宣告變數賦值,就是向電腦要一個記憶體空間來存值。

以平常的這個動作來說:var a = 'emma' ,其實是變數 a 指向電腦中某記憶體的位置(ex: 0x01) ,在這個記憶體位置中,儲存 emma 這個值。

如果我再宣告了一個 b:var b = a; ,雖然 b 和 a 的值一樣都是 'emma' ,但其實變數 b 是指向了另一個不一樣的記憶體位置 (ex:0x02),把 a 的值 copy 過來存,a 和 b 是存在於兩個獨立不同的記憶體位置中:

並基本型別有兩大特色:

  1. 只是一個值,不會有屬性
  2. 不可變異 (immutable)

怎麼說不可變異呢?如果我們改變了 a 的值,其實是改變 a 所指向的記憶體位置,'emma' 這個值是永遠不會被改變的,只是 a 所指向的記憶體內容不同了,這就是所謂的不可變異。

物件型別處理參考 by reference

除了基本型別,其他型別都算是物件型別,則會以 by reference 傳址的方式傳遞:

  • Object:物件
  • Array:陣列
  • Function:函式

假設我建立了一個陣列:var arr1 = [1,2,3] ,表示 arr1 指向了一個新的記憶體位置(ex: 0x01),而如果我再建立了第二個陣列並讓他等於 arr1: var arr2 = arr1 ,這時候 arr2 則會直接指向 arr1 的記憶體位置(0x01),所以不論 arr1 所儲存的值是多少, arr2 都會得到一樣的值:

ex:

var arr1 = [1,2,3];
var arr2 = arr1;
console.log(arr2); // [1,2,3]
arr1[0] = 2;
console.log(arr1); // [2,2,3]
console.log(arr2); // [2,2,3]

但是有一個很重要的例外,就是如果是直接以等號賦予新的值,就會再建立一個新的記憶體位置,在下面的範例中,arr1 和 arr2 將不再指向同一記憶體位置

var arr1 = [1,2,3];
var arr2 = arr1;
console.log(arr2); // [1,2,3]
arr1 = [4,5,6]; // 賦予新值
console.log(arr2); // [1,2,3]

若是以修改的方式則不算在此例外狀況內:

var obj1 = {name: 'emma'};
var obj2 = obj1;
console.log(obj2); // { name:'emma' }
obj1.age = 18; // 修改 obj1
console.log(obj1); // { name:'emma', age:18 }
console.log(obj2); // { name:'emma', age:18 }

Pass by sharing

在 Javascript 中的傳值和傳址之間其實有幾個萬年的爭議點:

  1. Javascript 只有 Pass by value
  2. Javascript 其實不是 Pass by value/reference,而是 Pass by sharing

先說結論:

不管 Javascript 是 Pass by value/reference/sharing,重點都應該在理解這幾種方式行為上的差異,而不是在名詞的定義上。每個人心中的定義不同,這是一個沒有標準答案的問題,如果你想要知道大家在吵什麼,可以再繼續往下看。

那 Pass by sharing 又是什麼?

剛剛在說明 pass by reference 的例外狀況時,就是爭議點中所認為,pass by sharing 可以完美解釋 Javascript 資料傳遞的地方。

以 pass by reference 的定義來說,就算我用等號直接定義了新的內容,引用到他的變數也應該要一起變動,但是我使用了等號和物件實字 (Object literal) 或是陣列實字 (array literal) 的方式改變值,卻讓這個變數指向了新的記憶體位置。

這個用大括弧直接賦予或定義物件值的方式,稱為物件實字 (Object literal)
ex: var emma = {age:18}
同理用中括弧直接賦予或定義陣列值的方式,稱為陣列實字 (Array literal)
ex: var fruits = []

pass by sharing 的定義就有點像是融合了call by value 和 call by reference:

  • 碰到基本型別,表現行為是 Pass by value。
  • 碰到物件型別,如果只是改變內容,表現行為是 Pass by reference,但是如果對物件作重新賦值(literal),表現行為是 Pass by value。

所以才會有這樣的論點,覺得 Javascript 其實是 call by sharing。

那 Javascript 只有 call by value 又是為什麼?

讓我們再看一次剛剛 call by reference 的示意圖:

其實以物件型別來說,變數儲存的都是記憶體位址,拿來找出位址中儲存的值,更底層的示意圖會展開像是這樣:

所以在 arr2 = arr1 的這個動作中,都是把 arr1 中儲存的值,複製一份丟進 arr2 中,不論複製的內容是「值」或是「位址」

這就是為什麼會有人認為,Javascript 一律都是 Pass by Value。在別的程式語言中,有的語言可以在傳遞資料的時候選擇要以 by value 或是 by reference 的方式傳遞,但是在 Javascript 中,是沒有辦法自己選擇的。

--

--

itsems
itsems_frontend

Stay Close to Anything that Makes You Glad You are Alive.