JavaScript 觀念 - 傳值、傳參考
什麼情況下是傳遞純值,什麼情況又是傳遞記憶體參考位址?
目錄:
JavaScript 型別
JS 支援的型別主要分為以下兩種:
- 原始型別 / 基本型別(Primitives):
string
、number
、boolean
、null
、undefined
、symbol
(ES6 新增),原始型別也表示這個資料是一個「純值」。 - 物件型別(Object):非基本型別的類型都屬於物件型別(陣列、函式都屬於此型別)
可透過 typeof 判斷值的型別:
1 |
|
原始型別的「傳值」(Call By Value)
1 |
|
以上面範例來說,變數 num2
的值是複製變數 num
的值而來,但是將變數 num
重新賦值後,變數 num2
的值並沒有跟著被改變。
原因是變數 num
的值屬於原始型別,JS 看到這個原始型別時,會幫變數 num2
建立一個新的記憶體空間,並「複製」變數 num
的值,最後址派給變數 num2
,此時兩個變數彼此是獨立的,所以即使變數 num
的值改變了,變數 num2
也不會受影響,這種情況稱為「傳值」。
物件型別的「傳參考」(Call By Reference)
1 |
|
從上述範例可以發現,同樣的行為下,如果換成物件型別,兩個變數的值都會一起被修改。
這是因為 JS 的物件,是透過「記憶體的參考位址」來傳遞資料的,示意圖如下:
當物件 { val: 5 }
指派給變數 obj
時,JS 會在記憶體某處建立這個物件,然後再將變數 obj
指向存放這個物件的記憶體位址,換句話說,實際上傳入變數 obj
裡面的值,是這個記憶體位址。
此時將變數 obj
指派給變數 obj2
時,變數 obj2
所傳入的值,也同樣是這個存放物件 { val: 5 }
的記憶體位址,而因為兩個變數都是指向同一個記憶體位址中的物件,所以當變數 obj
重新賦值的同時,變數 obj2
的值也會被修改,這種不同變數之間指向同一個記憶體位址的情況,稱為「傳參考」,或是「傳址」。
根據傳參考的特性,兩個物件因為指向同一個記憶體空間,因此當物件修改屬性值時,其他物件也會同步被修改,此時可以使用以下兩種方式來避免:
- 淺拷貝:只複製物件的第一層,第二層開始還是依照傳參考特性(指向的記憶體位置相同)。
- 深拷貝:複製物件,並且操作不影響原物件(指向的記憶體位置不同)。
淺拷貝範例一(Object.assign):
1 |
|
其中一種方式是透過 Object.assign
來複製原物件,從上述結果可以看到 obj2
在修改屬性值後,原物件的屬性值仍保值不變。
淺拷貝做法二(展開運算子):
1 |
|
另一種方式是使用 ...
將原物件展開並複製,結果與第一種方式相同。
前面有提到淺拷貝只會複製物件第一層,第二層開始還是還是只向相同的記憶體位置,如下方範例:
1 |
|
此時,可以使用深拷貝。
深拷貝範例:
1 |
|
深拷貝是使用 JSON.stringify
先將物件轉為純字串,再使用 JSON.parse
將純字串轉為物件,而從結果可以得知,因為兩物件的記憶體指向不同,因此物件修改屬性值後,原物件也不會受影響。
Call By Sharing
1 |
|
因為作為參數傳入函式的 obj 為物件型別,所以根據傳參考的特性,再函式內修改了屬性內容,會連帶影響到函式外的物件。
但是有一個例外,就是當傳入函式中的物件不是修改屬性內容,而是直接將物件重新賦值時,函式外的物件就不會被影響,範例如下:
1 |
|
函式外的變數 obj
作為參數傳入函式,接著在函式內進行重新賦值的行為,這代表函式內的 par
會重新指向一個新物件,而不是指向與函式外的 obj
相同的記憶體位址,示意圖如下:
以上情況非傳值(Call By Value)、也不屬於傳參考(Call By Reference),因此就衍生出了 Call By Sharing 的說法。
小結
- 原始型別指派給變數時,傳遞的是值的複製。
- 物件型別指派給變數時,傳遞的是記憶體的參考位址。
- 傳入函式內的物件,如果重新賦值,此時函式內、外物件之間的參考就會消失。