# JavaScript 的「傳值」與「傳址」
>[竹白記事本](https://chupai.github.io/),筆記紀錄。
###### tags: `JavaScript 竹白記事本`
這部分在 JavaScript 是相當重要的觀念,正確了解這個觀念能避免發生不必要的錯誤。
## ▶ 概述
在 Javascript 中,值的傳遞分為兩種:
- **傳值**,Call by value 或是 Pass by value。
- **傳址(傳參考)**,Call by reference 或是 Pass by reference。
在其他程式語言有語法可以決定要「傳值」還是「傳址」,但在 JavaScript 中沒有選擇,**基本型別** 就是「傳值」行為,**物件型別** 就是「傳址」行為。
## ▶ 傳值
當值的型別為 **基本型別**,那麼永遠都是藉由值的複製來賦值,所以稱作 **傳值**。
來看程式碼:
```javascript
var a = 10;
var b = 10;
a === b ; // console.log: true
```
- 由於 `10` 的型別為 **基本型別**;
- 當我們比較 `a` 與 `b` 時,是互相比較彼此的值,所以回傳 `true`。
繼續來看另一段程式碼:
```javascript
var a = 2;
var b = a;
b += 1;
a; // console.log: 2
b; // console.log: 3
```
- 當 `b` 指定為 `a` 時,由於 `a` 的值是 **基本型別**;
- 所以會 `b` 得到的是 `a` 的值而不是 `a` 的記憶體位址;
- 因此 `b` 就算改變了,`a` 也不會受到影響,兩者是獨立的。
## ▶ 傳址
接下來,我們來看 **物件型別** 的情況。
```javascript
var obj1 = { a: 1 };
var obj2 = { a: 1 };
obj1 === obj2; // console.log: false
```
你可以觀察到,就算 `obj1` 和 `obj2` 的屬性名稱與值都相同,但互相比較的結果卻是 `false`。
這是因為每個物件都是獨立存在的實體,兩者的記憶體位置並不相同。在比較 **物件型別** 時,比較的是記憶體位置,而非值。
繼續來看另一段程式碼:
```javascript
var obj1 = { a: 1 };
var obj2 = obj1;
obj1.a = 0;
obj2.a; // console.log: 0
obj2.b = 2;
obj1.b; // console.log: 2
obj1 === obj2; // console.log: true
obj1 = {};
obj1 === obj2; // console.log: false
```
- 將 `obj2` 透過 `obj2 = obj1` 的方式賦值;
- 當我們修改任意物件屬性時,另一邊的屬性也會更動;
- 這是因為兩個變數指向相同的記憶體位置,並沒有新的物件被複製出來;
- 但當我們將變數 `ob1` 賦值新的物件時,`obj1` 會指向新的記憶體位置;
- 而 `obj2` 依然保持原物件記憶體位置,這時 `obj1` 和 `obj2` 彼此之間就沒有關係了,所以比較的結果為 `false`。
**物件型別** 之間的傳遞是記憶體位置指向,所以稱作 **傳址**。
## ▶ 函式參數
如果一個 **物件型別** 作為函式參數傳遞,一樣是傳址行為:
```javascript
function foo(obj) {
obj.a = 10;
}
var obj1 = {};
foo(obj1);
obj1; // console.log: { a: 10 }
```
直接更改屬性,會影響原物件。
如果我們將參數重新指向新物件,那麼不影響原物件:
```javascript
function foo(obj) {
obj = { a: 10 };
}
var obj1 = {};
foo(obj1);
obj1; // console.log: {}
```
## ▶ 複製物件
物件型別是傳參考,那我們該如何複製物件呢?
複製物件的方式分為兩類:
- 淺拷貝(Shallow Copy)
- 深拷貝(Deep Copy)
### 1.淺拷貝操作
常見淺拷貝物件的方式:
- 使用 [`Object.assign()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 將原本的 `obj` 內容複製到一個空物件中。
- 在一個空物件內,使用 `...` 展開運算子展開物件。
```javascript
var obj = { foo: 1, bar: 2};
var obj2 = Object.assign({}, obj);
var obj3 = { ... obj };
obj === obj2; // console.log: false
obj2; // console.log: { foo: 1, bar: 2 }
obj === obj3; // console.log: false
obj3; // console.log: { foo: 1, bar: 2 }
```
常見淺拷貝陣列的方式:
- 利用會回傳新陣列的方法,例如 [`slice()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/slice)、[`concat()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/concat) [`map()`](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/map)等等。
- 在一個空陣列內,使用 `...` 展開運算子展開陣列。
```javascript
var arr = [1, 2, 3, 4];
var arr2 = arr.slice(0);
var arr3 = [].concat(arr);
var arr4 = arr.map((item)=> item);
var arr5 = [...arr];
arr === arr2; // console.log: false
arr2; // console.log: [1, 2, 3, 4]
arr === arr3; // console.log: false
arr3; // console.log: [1, 2, 3, 4]
arr === arr4; // console.log: false
arr4; // console.log: [1, 2, 3, 4]
arr === arr5; // console.log: false
arr5; // console.log: [1, 2, 3, 4]
```
::: success
**總結**,淺拷貝使用 `...` 展開運算子就對了,簡單易懂。
:::
### 2. 什麼是淺拷貝
以上拷貝方式有一個問題,請考慮下面程式碼:
```javascript
var obj1 = {
a: 10,
b: {
foo: 20,
},
};
var obj2 = { ...obj1 };
obj1 === obj2; // console.log: false
obj1.b === obj2.b; // console.log: true
```
假如物件內還有物件,就算我們使用 `...` 展開運算子複製物件,第二層物件還是指向相同的記憶體位置。
因為我們的複製操作只改變外層容器的地址,但沒動到內層,所以這種複製操作稱為「淺拷貝」。也就是說,當我們的物件或陣列是巢狀或多維的,要多注意。
### 3. 深拷貝
深拷貝就是完全複製一份,不會有共用記憶體的問題。
常見深拷貝物件方法有:
- 利用 JSON 方法:先轉 JSON 格式,再轉回來。
- 使用第三方函式庫:
- jQuery 的 [`$.extend()`](http://www.html.cn/jqapi-1.9/jQuery.extend/)
- Lodash 的 [`_.cloneDeep()`](https://www.lodashjs.com/docs/lodash.cloneDeep)
#### 3.1 JSON
先用 `JSON.parse()` 轉再 JSON 格式,再用 `JSON.stringify()` 轉回 JS 物件:
```javascript
var obj1 = {
a: 10,
b: {
foo: 20,
},
};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj1 === obj2; // console.log: false
obj1.b === obj2.b; // console.log: false
```
但有幾點要注意,當物件轉 JSON 格式時,
- 函式、`undefined`、`Symbol` 會被忽略;
- 而 `NaN`、`Infinity`、`-Infinity` 會被轉成 `null`。
所以利用 JSON 方法深拷貝物件,只能用在單純只有資料的物件。
#### 3.2 jQuery
jQuery 是常見的 JS 函式庫之一。
`jQuery.extend()`,是用來將兩個或更多物件的內容合併到第一個物件,我們可以用它來複製一個全新的物件。
```javascript
var obj1 = {
a: 10,
b: {
foo: 20,
},
};
var obj2 = $.extend(true, {}, obj1);
obj1 === obj2; // console.log: false
obj1.b === obj2.b; // console.log: false
```
第一個參數預設為 `fasle`(可省略),如果是 `false` 就是淺拷貝,`true` 為深拷貝。
#### 3.3 Lodash
Lodash 是熱門 JS 工具函式庫之一。
`_.cloneDeep(value)`,能深拷貝一個物件:
```javascript
var obj1 = {
a: 10,
b: {
foo: 20,
},
};
var obj2 = _.cloneDeep(obj1);
obj1 === obj2; // console.log: false
obj1.b === obj2.b; // console.log: false
```