JavaScript 中的 Call by sharing
A simple explanation of call by value, call by reference, and call by sharing in JavaScript, with memory diagrams and examples.
Call by value、Call by reference在一般的程式語言中是常見的基本觀念,但在JavaScript中多了一項Call by sharing,這裡做個簡單的說明。
1. Data Type
JavaScript 的資料類別可以分為:
- 基本型別(Primitive):string、number、boolean、null、undefined
- 物件型別(Object):object、symbol
基本型別會以純值的形式存在,而物件型別則是由零或不同型別的值所組成。
這邊之所以提及 Data Type 是因為資料類別會決定 call by value、call by reference、call by sharing 的運作時機。
2. 變數的真實樣貌
在提到 call by value 之前,我們先簡單講解平常看到的「變數」在程式中的真實樣貌。
當我們宣告一個變數時,如:let num = 1234,程式會自動分配一份記憶體空間,空間內部可以存放我們想要的資料,並且給予該空間一個名字。
以這段程式碼為例,num便是記憶體空間的名字(variable name),123是變數初始化後內部所存放的資料(variable value),另外還有我們平常看不到,用來標注空間所在位置的記憶體地址(memory address)。
3. 基本型別 & 物件型別
變數作為一個資料容器,當內部儲存的數值是基本型態時進行比較:
let a = "Dog";
let b = "Dog";
let c = "Cat";
// 變數之間進行數值比較
console.log(a === b); //true
console.log(b === c); //false
基本型態下兩者的比較結果可想而知,比較式會取出兩變數內的「值」進行比較,但是當變數內部儲存的值是物件型態時,比較結果會有所不同:
let a = {value: "Dog"};
let b = {value: "Dog"};
console.log(a === b); //false
明明變數 a, b 間的數值相同,但比較結果卻是false, 其實這就是 call by value 和 call by reference 之間的差異了。
4. Call by Value
當變數內的數值是基本型別時,JavaScript 使用 call by value 的方式對變數內的值進行更新與傳遞。
let a = 10
let b = a
console.log(`a=${a}, b=${b}`) // a=10, b=10
b = 20
console.log(`a=${a}, b=${b}`) // a=10, b=20
當 b 被初始化時,程式碼會分配一塊新的記憶體位置,並「複製」變數 a 內部的數值到新的記憶體空間中,因此儘管兩者的值是一樣的,兩個變數是實際上仍是指向完全不同的記憶體區塊,所以當變數 b 的數值更動時並不會影響到變數 a 內部的數值。

5. Call by reference
當變數內的數值是物件型別時,JavaScript 使用 call by reference 的方式對變數內的值進行更新與傳遞。 物件型別的資料與基本型別不同,可以彈性的進行擴充、修改與刪除,因此其記憶體空間的分配方式與單一變數不同,其實是多個不相鄰的記憶體空間串接而成,就像火車般由一節一節的車廂組成,我們可以隨意的新增、刪除車廂,或改變車廂的連接順序,變數所指向的不是車廂內的數值,而是火車的車頭,當我們要取用某節車廂的數值時,都要先透過車頭去找到對應的車廂,再取出其中的數值。 在下面的程式碼中,變數 a 與變數 b,都指向了同一個記憶體區塊,也就是同一輛火車頭,因此當其中的數值被修改時,透過 a 或 b 都會得到被修改後的結果(因為取得的數值都是來自同一節車廂)。
let a = {value: 10}
let b = a
console.log(a === b) // true,a與b所指向的記憶體位置相同
console.log(`a.value=${a.value}, b.value=${b.value}`) // a.value=10, b.value=10
b.value = 20 // 透過 b 對其中一個資料區塊的數值進行修改
console.log(`a.value=${a.value}, b.value=${b.value}`) // a.value=20, b.value=20

如此便可以解釋先前的程式碼了:
let a = {value: "Dog"};
let b = {value: "Dog"};
console.log(a === b); //false,a 與 b 指向的是完全不同的記憶體地址(兩者是不同的火車頭)
6. Call by sharing
Call by sharing可以說是call by reference中的特例,特點在於:當物件作為function的參數傳入時,如果對該物件在function內部中重新賦值,外部的物件的內容是不會被修改的。
範例1:function內部對物件進行修改
obj1 = {value: 100}
function changeValue(obj) {
obj.value = 300;
}
changeValue(obj1);
console.log(`obj1.value = ${obj1.value}`); // obj1.value = 300 數值被修變了
範例2:function內部對物件進行重新賦值
obj1 = {value: 100}
function giveNewValue(obj) {
obj = { value: 300 };
}
giveNewValue(obj1);
console.log(`obj1.value = ${obj1.value}`); // obj1.value = 100 數值不變
常見的 array 也是屬於物件型別,因此也符合 call by sharing 的運作規則:
let array1 = [0, 1, 2];
let array2 = [0, 1, 2];
function changeArrValue(arr) {
arr.push(4);
}
function giveNewArrValue(arr) {
arr = [0, 0, 0, 0, 0, 0];
}
changeArrValue(array1); // 修改矩陣
console.log(`array1 = ${array1}`); // array1 = [0,1,2,4] 數值被修變
giveNewArrValue(array2); // 矩陣重新賦值
console.log(`array2 = ${array2}`); // array2 = [0,1,2] 數值依然不變