本篇要記錄 JavaScript 相當重要的觀念「傳值與傳參考」,了解這個觀念是相當重要的,兩者都是討論關於變數的東西,讓我們開始吧。
傳值 ( By Value )
還記得純值 ( Primitives value) 是什麼東西嗎?
純值可以是:
- String
- Number
- Boolean
- null
- undefined
- Symbol
我們把其中一種純值設定到變數 a 中,所以現在這個變數 a 知道了這個純值的記憶體位址。
接著我們創造一個新的變數 b ,並且令 b = a ,變數 b 會指向一個新的記憶體位址,並拷貝那個純值,放到新的記憶體位址。
這種方式稱為傳值 ( By Value )
傳參考 ( By Reference)
在 JavaScript 中,所有的物件 (包含函式物件),全部都是傳參考的。
當設定一個變數 a 並且賦予值為物件類型,變數 a 仍然會得到物件的記憶體位址。
但當令b = a ,變數 b 此時不會得到一個新的記憶體位址,而是會指向變數 a 的記憶體位址,並不會創造新的拷貝物件。就好像別名一般,此時的 a 與 b 這兩個名稱都指向同一記憶體位址。簡單來說,此時的 a 與 b 的值是同樣的,因為它們指向相同的記憶體位址。
傳值的例子
// by value
var a = 3;
var b;
b = a;
console.log(a, b); // 3 3
透過上面的敘述,可以了解,為什麼 a 與 b 都是 3 了。
因為 3 是數值型別,所以當 b 被設定為 a 時,等號運算子看到 3 是純值,所以創造一個新的記憶體位址給 b ,接著拷貝 a 的值填入 b 的位址。
所以 a 是 3 、b 也是 3 ,但它們是對方的拷貝,在兩個不同的記憶體位址。
也就是說當 a 被更動時,b 不會受到影響。
// by value
var a = 3;
var b;
b = a;
console.log(a, b); // 3 3
a = 2;
console.log(a, b); // 2 3
傳參考的例子
var c = {
greeting: 'Hi'
};
var d = c;
console.log(c);
console.log(d);
我們給變數 c 設定了一個物件,同樣的,c 知道了物件的記憶體位址。當執行到 d = c 時,等號運算子看到物件不會創造新的記憶體位址給 d,而是把 d 指向和 c 相同的記憶體位址。
所以結果是相同的 ,但它們不是對方的拷貝,c 和 d只是指向相同的記憶體位址。
也就是說當 c 被更動時,d 也會受到影響。
// by reference (all objects (including functions))
var c = {
greeting: 'Hi'
};
var d = c;
console.log(c);
console.log(d);
c.greeting = 'Hello';
console.log(c);
console.log(d);
當物件用於函式的參數上時
當物件用於函式的參數上時,物件也是透過傳參考的方式被傳入,觀察一個例子:
var c = {
greeting: 'Hi'
};
var d = c;
c.greeting = 'Hello';function changeGreeting(obj){
obj.greeting = 'Hola';
}
changeGreeting(d);
console.log(c);
console.log(d);
我們傳入變數 d 到函式中,此時 obj 會指向 d 的記憶體位址,但接續前面的例子,d 已經指向 c 的記憶體位置,而 c 被設定了一個物件。
所以當使用 obj.greeting 改變了值,表示會更新這個物件所指向的記憶體位址內的值,因此輸出 c 與 d 的值,可以發現都被改變了。
例外情況一
有件事情要特別注意,使用等號運算子賦予新值(記憶體還不存在的值)時,會設定一個新的記憶體位址,接續上面的例子:
var c = {
greeting: 'Hi'
};
var d = c;
c.greeting = 'Hello';function changeGreeting(obj){
obj.greeting = 'Hola';
}
changeGreeting(d);
console.log(c);
console.log(d);c = {
greeting: 'Howdy'
}
console.log(c);
console.log(d);
我使用等號運算子設定變數 c 為一個新的值,然後等號運算子會設定一個新的記憶體空間給 c ,並且放進那個值。自此, d 和 c 就不再指向同一個記憶體位址。
所以這是一個特殊的例子,這並不是傳參考。
等號運算子看到 { greeting: 'Howdy' } 還不存在於記憶體,這是一個創造物件的物件實體語法,所以並不是一個已經存在的物件。因此等號運算子必須建立另一個新的記憶體空間給物件,然後指向 c 。
與例子上半部 d = c 不同的地方是 c 已經存在了
因此等號運算子知道 c 已經在記憶體中,不需要另外創造記憶體空間,而且 c 是個物件,只要把 d 指向同一個位址就好。
例外情況二
我們延伸例外情況一,使之變得更為複雜:
var c = {
greeting: 'Hi'
};
function changeGreeting(obj){
obj = {
greeting: 'Hola'
}
}
changeGreeting(c);
console.log(c);
這個答案是我們想的那樣嗎?
答案不是 { greeting: "Hola" } ,為什麼?
我們使用物件實體語法創造一個物件並且令變數 c 指向自身記憶體位址。
接著我們知道當物件用於函式的參數上時是傳參考的。因此此時的 obj 與 c 指向同一個物件的記憶體位址。
但是,當程式碼執行到
obj = {
greeting: 'Hola'
}
同例外情況一看到的,等號運算子看到 { greeting: 'Hola' } 還不存在於記憶體,這是一個創造物件的物件實體語法,所以並不是一個已經存在的物件。因此等號運算子必須建立另一個新的記憶體空間給物件,然後指向 obj。
因此這個時候 obj 已經與 c 指向不同的記憶體位址了,自然 c 指向的物件並不會被改變。
後記:
在寫這篇的時候,發現到有些文章好像對於傳值、傳參考的細節描述都有一些些不同的地方,像是這篇文章 - 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?寫得很仔細,而且其實技術的名詞定義紛爭也是不少,像是這篇。
最後擷取一段胡立大大文章的句子作為例外情況的總結:
JavaScript 傳 object 進函式的時候,可以更改原本 object 的值,但重新賦值並不會影響到外部的 object