0%

JS核心-(27)-物件-Call by Reference 還是 Call by Sharing

前言

變數內容的傳遞方式是 Pass by value (傳值) 或 Pass by reference (傳址),對於程式開發是一個很基本、但極為重要的觀念

什麼是Pass by value (傳值)

先來看一個 Pass by value 的例子

var x = 5;
var y = x;
console.log(x);     // 5
console.log(y);     // 5

x = 10;
console.log(x);     // 10
console.log(y);     // 5
  • 變數 x 被賦予 5 這個數值,然後透過 y = x 的方式為變數 y 賦值,因此 xy 一開始的變數內容都是 5

  • 接著針對 x 賦予新的值 10,此時 x10,但 y 仍是 5

為什麼改變 x 的值,y 會不受影響?

因為當變數內容傳遞方式是 Pass by value (傳值),也就是將舊變數的值內容複製一份,放進一塊新的記憶體,讓新變數指向過去。

以上面的例子來說,就是將變數 y 指向一塊新的記憶體,然後將變數 x 裡的值複製一份放進去,等於變數 xy 裡存放的是兩份獨立的資料,其中一份改變,不會影響另一份。


什麼是Pass by reference (傳址)

先來看一個 Pass by reference 的例子

var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

person1.money = 222;
console.log(person1);  // {money: 222}
console.log(person2);  // {money: 222}
  • 變數 person1 賦予一個物件,透過 person2 = person1 的方式為 person2 賦值,person1person2 兩個物件的 money 屬性一樣都是 111
  • 針對 person1.money 更換值的內容,連 person2 的屬性也一起變動到

因為當變數內容傳遞方式是 Pass by reference (傳址),新變數會直接指向舊變數的記憶體位址,等於新舊變數共用同一個位址的資料。

所以 person1person2 其實指向同一個物件,就像同一個房間有兩個入口,從哪一個入口進去,看到的都是同一個房間內容。因此,對 person1 作任何變動,也等同對 person2 作變動。


JavaScript 什麼時候是 Pass by value?什麼時候是 Pass by reference?

基本上看變數的資料型別,決定傳遞行為是 Pass by value 或 Pass by reference。

當變數的值是原生型別 (Primitive) 時,行為是 Pass by value

在 JavaScript 中,原生型別 (Primitive) 包含:

  • String
  • Number
  • Boolean
  • Undefined
  • Null

null 雖然用 typeof 運算的結果是回傳 object,但 null 在行為上歸屬原生型別。

以下是這五種型別的行為測試:

/* string */
var str1 = "111";
var str2 = str1;
console.log(str1); // "111"
console.log(str2); // "111"

str1 = "222";
console.log(str1); // "222"
console.log(str2); // "111"


/* number */
var n1 = 111;
var n2 = n1;
console.log(n1); // 111
console.log(n2); // 111

n1 = 222;
console.log(n1); // 222
console.log(n2); // 111


/* boolean */
var bool1 = true;
var bool2 = bool1;
console.log(bool1); // true
console.log(bool2); // true

bool1 = false;
console.log(bool1); // false
console.log(bool2); // true


/* null */
var nu1 = null;
var nu2 = nu1;
console.log(nu1); // null
console.log(nu2); // null

nu1 = "something";
console.log(nu1); // "something"
console.log(nu2); // null


/* undefined */
var ud1 = undefined;
var ud2 = ud1;
console.log(ud1); // undefined
console.log(ud2); // undefined

ud1 = "something";
console.log(ud1); // "something"
console.log(ud2); // undefined

當變數的值是物件型別 (Object) 時,行為是 Pass by reference

在 JavaScript 中的物件型別常見的例如:

  • Array
  • Object

以下是這兩種型別的行為測試:

/* array */
var ary1 = [1, 2, 3];
var ary2 = ary1;
console.log(ary1); // [1, 2, 3]
console.log(ary2); // [1, 2, 3]

ary1[0] = 99;
console.log(ary1); // [99, 2, 3]
console.log(ary2); // [99, 2, 3]


/* object */
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

person1.money = 222;
console.log(person1);  // {money: 222}
console.log(person2);  // {money: 222}

Pass by sharing 又是怎麼一回事?

JavaScript 裡,原生型別 (Primitive) 是 Pass by value,物件型別 (Object) 是 Pass by reference。

怎麼會冒出一個 Pass by sharing 的說法?我們來看以下的情境

1. 函數參數傳遞

function rename(obj){
    obj = { name: "XXX" };
}

var person = { name: "Bob" };
console.log(person);
rename(person);
console.log(person);

上面這個範例,將物件 person 傳遞給函數 rename() 作為參數,並在 rename() 裡面改變值。

根據前面的結論,物件型別 (Object) 是 Pass by reference,那麼執行結果應該是先印 {name: “Bob”} 再印 {name: “XXX”} 囉?

執行結果:

{ name: "Bob" }
{ name: "Bob" }

怎麼好像和前面說好的不一樣?

上述例子可以再作抽離,縮小範圍,只看對 obj 變更值的部分。例子中是使用物件實字 (Object Literals) 的方式來變更值。

2. 使用陣列實字 (Array Literals) 和物件實字 (Object Literals) 重新賦值

/* array literals */
var ary1 = [1, 2, 3];
var ary2 = ary1;
console.log(ary1); // [1, 2, 3]
console.log(ary2); // [1, 2, 3]

ary1 = [99, 100];
console.log(ary1); // [99, 100]
console.log(ary2); // [1, 2, 3]


/* object literals */
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

person1 = { money: 222 };
console.log(person1);  // {money: 222}
console.log(person2);  // {money: 111}

可以發現到,雖然是物件型別 (Object) 的變數,如果用實字 (Literals) 對變數作重新賦值,並不會連另一個變數一起變更

3. 使用第三方變數作重新賦值  

即使不是直接用實字 (Literals),而是透過第三方變數去作重新賦值,同樣不會影響到第二個變數:  

/* array re-assigned */
var ary1 = [1, 2, 3];
var ary2 = ary1;
console.log(ary1); // [1, 2, 3]
console.log(ary2); // [1, 2, 3]

var ary3 = [99, 100];
ary1 = ary3;
console.log(ary1); // [99, 100]
console.log(ary2); // [1, 2, 3]
console.log(ary3); // [99, 100]


/* object re-assigned */
var person1 = { money: 111 };
var person2 = person1;
console.log(person1);  // {money: 111}
console.log(person2);  // {money: 111}

var person3 = { money: 333 };
person1 = person3;
console.log(person1);  // {money: 333}
console.log(person2);  // {money: 111}
console.log(person3);  // {money: 333}

從前面例子發現到,雖然是物件型別 (Object) 的變數,如果是對物件變數作重新賦值,只會變更自己的值,

不會連另一個變數一起變更。這和前面提到的 Pass by reference 行為似乎不太一樣,反而有點像 Pass by value。

如此一來,稱為 Pass by reference 也不對,稱為 Pass by value 也不對,於是就出現了 Pass by sharing 的說法。


不少人將 JavaScript 的變數內容傳遞方式,稱為 Pass by sharing:

  • 碰到原生型別 (Primitive),表現行為是 Pass by value。

  • 碰到物件型別 (Object),如果只是對物件內容作操作(例如陣列元素或物件屬性),表現行為是 Pass by reference。

  • 碰到物件型別 (Object),如果對物件作重新賦值,表現行為是 Pass by value。


或者也有人視為:JavaScript 的 Primitive 是 Pass by Value,Object 是 Pass by sharing。

那為何有人說 JavaScript 只有 Pass by value?

答案都是對的,視乎立足的定義和觀點。會有這麼多技術名詞上的歧義,就像這篇文章所言:

程式開發的世界中,名詞的創造經常是隨意的。
這些名詞通常沒有明確的定義,一開始只是為了溝通方便,而隨著聽聞者各自的領會與轉述,加上時間與歷史的推移,
它們的定義會發生岐義而各自發展,最後失去了溝通的意義。


結論  

技術名詞是為了方便溝通,不是為了吵架。

技術名詞的概念統一定義很重要,因為能讓大家在一個共通的語彙概念上溝通或學習。

更詳盡的介紹我覺得可以看看這篇文章 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

參考資料