<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 的時候,意義上非常類似的。
相較於名詞,更重要的是,你要清楚知道背後的原理與對實務開發上的影響。