[JavaScriptWeird]No.27 觀念小叮嚀:傳值和傳參考

前言

本篇要記錄 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 的記憶體位址,並不會創造新的拷貝物件。就好像別名一般,此時的 ab 這兩個名稱都指向同一記憶體位址。簡單來說,此時的 ab 的值是同樣的,因為它們指向相同的記憶體位址。

傳值的例子

1
2
3
4
5
// by value  
var a = 3;
var b;
b = a;
console.log(a, b); // 3 3

透過上面的敘述,可以了解,為什麼 ab 都是 3 了。

因為 3 是數值型別,所以當 b 被設定為 a 時,等號運算子看到 3 是純值,所以創造一個新的記憶體位址給 b ,接著拷貝 a 的值填入 b 的位址。

所以 a 是 3 、 b 也是 3 ,但它們是對方的拷貝,在兩個不同的記憶體位址。

也就是說當 a 被更動時, b 不會受到影響。

1
2
3
4
5
6
7
// by value  
var a = 3;
var b;
b = a;
console.log(a, b); // 3 3
a = 2;
console.log(a, b); // 2 3

傳參考的例子

1
2
3
4
5
6
var c = {  
greeting: 'Hi'
};
var d = c;
console.log(c);
console.log(d);

我們給變數 c 設定了一個物件,同樣的, c 知道了物件的記憶體位址。當執行到 d = c 時,等號運算子看到物件不會創造新的記憶體位址給 d ,而是把 d 指向和 c 相同的記憶體位址。

所以結果是相同的 ,但它們不是對方的拷貝, cd 只是指向相同的記憶體位址。

也就是說當 c 被更動時, d 也會受到影響。

1
2
3
4
5
6
7
8
9
10
// 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);

當物件用於函式的參數上時

當物件用於函式的參數上時,物件也是透過傳參考的方式被傳入,觀察一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
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 改變了值,表示會更新這個物件所指向的記憶體位址內的值,因此輸出 cd 的值,可以發現都被改變了。

例外情況一

有件事情要特別注意,使用等號運算子賦予新值(記憶體還不存在的值)時,會設定一個新的記憶體位址,接續上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ,並且放進那個值。自此, dc 就不再指向同一個記憶體位址。

所以這是一個特殊的例子,這並不是傳參考。

等號運算子看到 { greeting: 'Howdy' } 還不存在於記憶體,這是一個創造物件的物件實體語法,所以並不是一個已經存在的物件。因此等號運算子必須建立另一個新的記憶體空間給物件,然後指向 c

與例子上半部 d = c 不同的地方是 c 已經存在了

因此等號運算子知道 c 已經在記憶體中,不需要另外創造記憶體空間,而且 c 是個物件,只要把 d 指向同一個位址就好。

例外情況二

我們延伸例外情況一,使之變得更為複雜:

1
2
3
4
5
6
7
8
9
10
var c = {  
greeting: 'Hi'
};
function changeGreeting(obj){
obj = {
greeting: 'Hola'
}
}
changeGreeting(c);
console.log(c);

這個答案是我們想的那樣嗎?

答案不是{ greeting: "Hola" } ,為什麼?

我們使用物件實體語法創造一個物件並且令變數 c 指向自身記憶體位址。

接著我們知道當物件用於函式的參數上時是傳參考的。因此此時的 objc 指向同一個物件的記憶體位址。

但是,當程式碼執行到

1
2
3
obj = {  
greeting: 'Hola'
}

例外情況一看到的,等號運算子看到 { greeting: 'Hola' } 還不存在於記憶體,這是一個創造物件的物件實體語法,所以並不是一個已經存在的物件。因此等號運算子必須建立另一個新的記憶體空間給物件,然後指向 obj

因此這個時候 obj 已經與 c 指向不同的記憶體位址了,自然 c 指向的物件並不會被改變。

後記:

在寫這篇的時候,發現到有些文章好像對於傳值、傳參考的細節描述都有一些些不同的地方,像是這篇文章 - 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?寫得很仔細,而且其實技術的名詞定義紛爭也是不少,像是這篇

最後擷取一段胡立大大文章的句子作為例外情況的總結:

JavaScript 傳 object 進函式的時候,可以更改原本 object 的值,但重新賦值並不會影響到外部的 object

0%