---
# System prepended metadata

title: JS 物件參考 by reference
tags: [物件, JS]

---

<style> 
    body{font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft JhengHei', Roboto, 'Helvetica Neue', Arial, sans-serif;}
    .red-color{color:#f00}
    .blue-color{color:#f00}
</style>
###### tags: `JS`,`物件`
# JS 物件參考 by reference
> [time=Fri, Feb 21, 2020 12:25 PM]
> [color=#000] [name=aikwdc00]
> 

## 變數如何儲存物件？
至於物件是不同情況，基本型別是一個值，直接把值放在變數裡就好了，就像一個盒子裡放一個容器；但物件裡不只有一個值，物件可以透過 key-value pair 的形式來裝各種類似的資料。

因此物件需要使用更複雜的容器，而這個容器也會存放在記憶體裡不同的位置，一個叫做 heap memory 的地方。

讓我們先看一個例子，我們宣告了一個物件 a，然後將 a 賦值給 b，接著修改物件 a 屬性裡的值：

```javascript=
const a = { foo: 1 }
const b = a
a.foo = 2
```
以下是示意圖，請你從圖中觀察以下三件事：

* 變數 a 和 b 裡存放的是物件在記憶體裡的參照位址 (reference)
* 當 a 把物件位址拷貝給 b 時，被拷貝的是這個參照位址，兩者指向同一個地方
* 物件的屬性內容改變時，a 和 b 的內容——也就是那個參照位址，並沒有改變

[<img src="https://assets-lighthouse.alphacamp.co/uploads/image/file/9415/ExportedContentImage_03.png">](https://assets-lighthouse.alphacamp.co/uploads/image/file/9415/ExportedContentImage_03.png)

* 為什麼以 const 來宣告物件時，仍然可以改寫物件的屬性。對變數 a 來說，「參照位址」沒有改變，改變的是「放在參照位址的物件屬性」。

* 由於物件參照位址通常不會改變，因此，我們的確更常用 const 來宣告一個新物件。

### other example
#### 第一個案例
```javascript=
const family = {
  home: 'home',
  members: {
    father: 'father',
    mother: 'mother',
    ming: 'small ming'
  }
}

let member = family.members
member = {ming: 'big ming'}

console.log(member, family.members)
// {ming: "big ming"} 
// {father: "father", mother: "mother", ming: "small ming"}

// 上述案例為family.members指派給member，這時member的值 = family.members
// 但因為又宣告member = {ming: 'big ming'},member則會參考新的參照位置
// 若改為 member.ming = 'big ming' ， member 與 family.members 仍參照同一個位置。
```
#### 第二個案例
```javascript=
const a = {x: 1}
a.y = a

console.log(a)

// 輸出結果
{x: 1, y: {…}}
x: 1
y:
    x: 1
    y: {x: 1, y: {…}}
// 由於a.y = x ，所以y會參照x，輸出結果y會無限循環
```
#### 第三個案例
```javascript=
let a = {x: 1}
let b = a
a.y = a = { x: 2 } 
// a無法用const宣告，因為a = { x: 2 }等於指派新的參照位置

console.log(a, b)
console.log(a.y) // undefined
//輸出結果
// {x: 2} , Object {x: 1, y: {x: 2}}
// 第三行由於a = { x: 2 }，所以a參照新的記憶體 {x:2}
// 呈上，a.y會找原本參照的記憶體位置，賦予a.y:{x: 2}
```

#### 第四個案例
```javascript=
let letter = {
  name: 'home',
  family: {
    father: 'father',
    mother: 'mother',
    ming: 'ming'
  }
}

var newFamily = {}

for (let i in letter) {
  newFamily[i] = letter[i]
  // 淺拷貝，第一層參照是不同位置，第二層仍參照相同位置。
}
console.log(letter, newFamily)
newFamily.family.jay = 'jay'
console.log(letter, newFamily)
// ming's home
// {name: "home", family: {…}}
// name: "home"
// family:
// father: "father"
// mother: "mother"
// ming: "ming"
// jay: "jay"
// __proto__: Object
// __proto__: Object
// ----------------------
// jay' home 
// {name: "home", family: {…}}
// name: "jays's home"
// family:
// father: "father"
// mother: "mother"
// ming: "ming"
// jay: "jay"
// __proto__: Object
// __proto__: Object
```
### 修改 Primitive data 和 Object data 的變數的差異
1. 可以使用 const 來宣告物件，
2. 另一個是你要注意資料會在什麼時候被更新。

[<img src="https://assets-lighthouse.alphacamp.co/uploads/image/file/9416/ExportedContentImage_04.png">](https://assets-lighthouse.alphacamp.co/uploads/image/file/9416/ExportedContentImage_04.png)

相比於原型型別，當你存放的是物件參照位址時，變數本身是不知道物件內容的，變數只知道如何取得物件，若你需要進一步操作物件，你必然會透過 . 來呼叫物件的屬性或方法。

### So What?
* 原始資料型別（純值）是 copying by value
* 而物件是 copying by reference

### 陣列
同樣的邏輯，但如果我們換成陣列 (array) 的話（注意，陣列是物件的一種)：
[<img src="https://assets-lighthouse.alphacamp.co/uploads/image/file/9417/ExportedContentImage_05.png">](https://assets-lighthouse.alphacamp.co/uploads/image/file/9417/ExportedContentImage_05.png)


這是因為在 heap memory 的內容被更新了，所以當你呼叫 a 跟 b的變數名稱時，兩個變數都會去到記憶體裡同一個位置，找到同樣的內容：
[<img src="https://assets-lighthouse.alphacamp.co/uploads/image/file/9418/ExportedContentImage_06.png">](https://assets-lighthouse.alphacamp.co/uploads/image/file/9418/ExportedContentImage_06.png)

### 若在函式裡改變物件，物件仍然會被改變
同樣的道理也會發生在函式裡。這是特別需要注意的地方，之前解釋函式時，我們談過當引數傳遞給函式時，是用 copy by value 的方式，但請回想上文 const b = a 的例子，當 reference 被拷貝時，仍然都指向同一個物件。

在下例中，傳給 editSpec 的物件在函式執行時被改變了：

```javascript=
const myPhone = {
  name: 'myPhone',
  price: 14999
}
function editSpec (item) {
  item.name = 'new name'
  item.price = 0
}
editSpec(myPhone)
console.log(myPhone) // {name: "new name", price: 0}
```
詳細情況請看圖解：
[<img src="https://assets-lighthouse.alphacamp.co/uploads/image/file/9419/ExportedContentImage_07.png">](https://assets-lighthouse.alphacamp.co/uploads/image/file/9419/ExportedContentImage_07.png)



意識到 copied by value 和 copied by reference 的區分，因此你才能清楚意識到變數內容什麼時候會被改變。

你無法直覺地複製物件
另一件要注意的事情，是你無法使用以下方式來拷貝出第二個物件，b 和 a 指向同一個 reference，因此物件仍然只有一個：

```javascript=
const a = { foo: 1 }
const b = a
```
若你真的想要從 a 出發，做出一個 reference 不一樣的變數，你會需要使用 **Object.assign**：
```javascript=
let a = { foo: 1 }
let clone = {}
Object.assign(clone, a)
```
這樣在 clone 裡的物件才會是一個 reference 不一樣的物件。
* **Object.assign**只能處理深度只有一層的物件

### 淺拷貝 與 深拷貝
#### 淺拷貝
* 只複製指向某個物件的指標，而不複製物件本身
* 新舊物件還是共用同一塊記憶體
* 三種淺拷貝的方式，只能處理深度只有一層的物件
    * 宣告空物件，用for...in迭代後並指派給空物件
    * Object.assign
    * 用jquery的extend
#### 深拷貝
* 但深拷貝會另外創造一個一模一樣的物件
* 新物件跟原物件不共用記憶體
* 修改新物件不會改到原物件
* 可透過josn方式，轉字串裡面包括轉字串，傳參照的特性就會消失
    * JSON.parse(JSON.stringify(需要複製的物件)

#### 參考文章
> [JS-淺拷貝(Shallow Copy) VS 深拷貝(Deep Copy)](https://kanboo.github.io/2018/01/27/JS-ShallowCopy-DeepCopy/)
> [淺層複製及深層複製](https://ithelp.ithome.com.tw/articles/10229589)
> [js对象的深度克隆的三种方法（深拷贝）](https://blog.csdn.net/huchangjiang0/article/details/79990068)

最後要補充的是，因為 JavaScript, 以及 Python, Ruby, Java 等語言其實沒有 reference 這種低階的 type (C, C++ 等才有)。如果你查看[維基百科](https://en.wikipedia.org/wiki/Evaluation_strategy)，by reference 正式的名稱應該是 by sharing 或是 by object，但「by sharing」這個詞在業界的使用不普遍。網路上很多資源也直接稱為 by reference。如果你在讀文件時，看到 JavaScript 是 copy by reference 或是 copy by sharing 的時候，意義上非常類似的。

相較於名詞，更重要的是，你要清楚知道背後的原理與對實務開發上的影響。
