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