前言
本篇要記錄 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
的值是同樣的,因為它們指向相同的記憶體位址。
傳值的例子
1 | // by value |
透過上面的敘述,可以了解,為什麼 a
與 b
都是 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 | var c = { |
我們給變數 c
設定了一個物件,同樣的, c
知道了物件的記憶體位址。當執行到 d = c
時,等號運算子看到物件不會創造新的記憶體位址給 d
,而是把 d
指向和 c
相同的記憶體位址。
所以結果是相同的 ,但它們不是對方的拷貝, c
和 d
只是指向相同的記憶體位址。
也就是說當
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 | var c = { |
我們傳入變數 d
到函式中,此時 obj
會指向 d
的記憶體位址,但接續前面的例子, d
已經指向 c
的記憶體位置,而 c
被設定了一個物件。
所以當使用 obj.greeting
改變了值,表示會更新這個物件所指向的記憶體位址內的值,因此輸出 c
與 d
的值,可以發現都被改變了。
例外情況一
有件事情要特別注意,使用等號運算子賦予新值(記憶體還不存在的值)時,會設定一個新的記憶體位址,接續上面的例子:
1 | var c = { |
我使用等號運算子設定變數 c
為一個新的值,然後等號運算子會設定一個新的記憶體空間給 c
,並且放進那個值。自此, d
和 c
就不再指向同一個記憶體位址。
所以這是一個特殊的例子,這並不是傳參考。
等號運算子看到 { greeting: 'Howdy' }
還不存在於記憶體,這是一個創造物件的物件實體語法,所以並不是一個已經存在的物件。因此等號運算子必須建立另一個新的記憶體空間給物件,然後指向 c
。
與例子上半部 d = c
不同的地方是 c
已經存在了
因此等號運算子知道 c
已經在記憶體中,不需要另外創造記憶體空間,而且 c
是個物件,只要把 d
指向同一個位址就好。
例外情況二
我們延伸例外情況一,使之變得更為複雜:
1 | var c = { |
這個答案是我們想的那樣嗎?
答案不是{ greeting: "Hola" }
,為什麼?
我們使用物件實體語法創造一個物件並且令變數 c
指向自身記憶體位址。
接著我們知道當物件用於函式的參數上時是傳參考的。因此此時的 obj
與 c
指向同一個物件的記憶體位址。
但是,當程式碼執行到
1 | obj = { |
同例外情況一看到的,等號運算子看到 { greeting: 'Hola' }
還不存在於記憶體,這是一個創造物件的物件實體語法,所以並不是一個已經存在的物件。因此等號運算子必須建立另一個新的記憶體空間給物件,然後指向 obj
。
因此這個時候 obj
已經與 c
指向不同的記憶體位址了,自然 c
指向的物件並不會被改變。
後記:
在寫這篇的時候,發現到有些文章好像對於傳值、傳參考的細節描述都有一些些不同的地方,像是這篇文章 - 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?寫得很仔細,而且其實技術的名詞定義紛爭也是不少,像是這篇。
最後擷取一段胡立大大文章的句子作為例外情況的總結:
JavaScript 傳 object 進函式的時候,可以更改原本 object 的值,但重新賦值並不會影響到外部的 object