Call by value v.s. Call by reference

淺談變數的傳遞方式差異與注意事項。

在變數指定的時候,一般都是 Call by value。

但如果遇到 Object 型態,基本上就會是 Call by reference。

Call by value

宣告一個變數後,記憶體會切一塊位置給此變數存放內容。

var a = 1;
// 使用記憶體位置 0x01 存放變數 a 存放 value 為 1

var b = a;
// 使用記憶體位置 0x02 存放變數 b,並且把 a 的 value 給 b
 
b = 90;
// 將 value 90 寫入 b 的記憶體位置 0x02

console.log(a); // 1
console.log(b); // 90

雖然使用 var b = a; 來指定 a 的 value,但是 a 跟 b 是獨立記憶體位置,所以後面再去異動 b 時,跟 a 毫無關係。

Call by reference

宣告一個物件或是陣列後,會先切一塊記憶體存放變數,在切一塊記憶體存放物件內容。

為什麼陣列也是 Call by reference ?

因為他也是 Object 的一種,從記憶體分配跟存取方式就可以推測出來。

var foo = {name: 'into', age: 18};
// 使用記憶體位置 0x01 存放變數 foo
// 使用記憶體位置 0x02 存放物件內容 {name: 'into', age: 18}
// 將 foo 記憶體位置 0x01 value 寫入記憶體指標 0x02

var bar = foo;
// 使用記憶體位置 0x03 存放變數 bar
// 將 bar 記憶體位置 0x03 value 寫入記憶體位置 0x01 的 value => 記憶體指標 0x02

這邊可以發現 foo 跟 bar 的 value 都是 記憶體指標 0x02。

此時修改 bar.name 為 good,原先我們預期只有更改 bar.name 的值。

bar.name = 'good';
console.log(bar.name); // good

console.log(foo.name); // good !?

結果卻是連同 foo.name 都被改成 good 了 !?

這是因為現在 foo 跟 bar 的記憶體位置 value 都是記憶體指標 0x02,所以都是參考同一位置。異動後,其他變數只要是記憶體指標指向 0x02 都會被異動。

深層複製

要處理這種情況,一般會使用 for loop 來將 value 做複製。

例如

var foo = {name: 'into', age: 18};
var bar = {};

for (var i in foo) {
    bar[i] = foo[i];
}

這樣就可以做到複製效果,讓兩個變數內容一樣,但是實際上記憶體指標是脫鉤的。

var foo = {name: 'into', age: 18, cards: [1,2,3,4]};
var bar = {};

for (var i in foo) {
    bar[i] = foo[i];
}

bar.cards[0] = 99;

console.log(bar.cards); // [99,2,3,4]

console.log(foo.cards); // [99,2,3,4] !?

看似完美的複製,遇到多層物件的時候就破功了,這是淺層複製的問題所在。

兩個深層複製的方法

  • 使用 recusive 遞迴方式處理每一個物件或是陣列
  • 使用 var bar = JSON.parse(JSON.stringify(foo))

第二個方法要記得因為先轉為 JSON 再轉回來,所以要是有 Function 會因為這樣消失喔!