一、什么是值類型?
二、什么是引用類型?
三、使用ES Next新特性帶來的 Object.assign 方法 和 擴展運算符;
四、Object.assign 方法 和 擴展運算符的 “深入淺出” 問題 —— 淺拷貝;
五、解決深拷貝問題常見的三種方法。
在 JavaScript 中,數據類型包括:
1、基本數據(值)類型,如 undefined、null、number、boolean、string等
2、引用類型:如Object、Array、Function等。
一、什么是值類型?
var a = 1; var b = a; b = b + 1 console.log(a, b) // 1, 2
值類型賦值時,會在棧內存空間中分配全新的地址,並得到相應的值。代碼中 b 是從 a 創建來的,當 b 變化時,a 不會受到影響。這是符合常識的。
二、什么是引用類型?
var data = { id: 0, book: 'learn redux', avaliable: false }
var newData = data
newData.id = 1; console.log(data.id, newData.id) // 1 1
再來看看引用類型(以Object對象為例),對象實際是存在於堆內存空間中。當我們要訪問一個對象的時候,實際上是從棧內存中獲取引用地址,然后根據這個引用地址再從堆內存中獲取所需要的值。
而引用類型賦值時,實際上是獲取其引用地址,而不是直接獲取值。所以,值發生改變時,源對象也會隨着改變。
三、使用ES Next新特性帶來的 Object.assign 方法 和 擴展運算符
使用 Object.assign({}, ...) 來解決 "單層"對象 引用問題
let data = { id: 0, book: 'learn redux', avaliable: false }
let newData = Object.assign({}, data)
newData.id = 1; console.log(data.id, newData.id) // 0 1
或者使用對象的擴展運算符
var data = { id: 0, book: 'learn redux', avaliable: false } var newData = {title: 'fuck', ...data}; newData.id = 1; console.log(data.id, newData.id) // 0 1
(PS:JavaScript的數組也是引用類型,同理可以使用 Array.concat 方法 和 數組的擴展運算符來解決引用問題,這里就省略了)
四、Object.assign 方法 和 擴展運算符的 “深入淺出” 問題 —— 淺拷貝
需要注意的是,使用 Object.assign 及 擴展運算符都屬於淺操作。如果往對象的外面再套一層的話,那問題就會浮現出來了。
var data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } } var newData = Object.assign({}, data) newData.item.id = 1; console.log(data.item.id, newData.item.id) // 1 1
上述的結果表明,原始數據還是被更改了。

五、解決深拷貝問題常見的三種方法
1、jQuery的深拷貝工具: $.extend(true, ...)
let data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } }
// 首參必須設置為 true 才算是深拷貝 let newData = $.extend(true, {}, data); newData.item.id = 1 console.log(data.item.id, newData.item.id) // 0 1
為什么$.extend默認不使用深拷貝,還需要手動傳參true才開啟呢?
這是因為深拷貝在實戰中是比較消耗性能的,如無必要請使用淺拷貝即可。稍后我們會實現一個自己的深拷貝工具。你可以從中了解詳情。

2、巧用序列化:JSON.parse(JSON.stringify(...))
let data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } } let newData = JSON.parse(JSON.stringify(data)) newData.item.id = 1 console.log(data.item.id, newData.item.id) // 0 1
原理很簡單,就是先將對象轉換為字符串,再重新序列化為對象,這樣理所當然不會有引用問題。值得一提的是,這些操作對數組也是有效的。

3、遞歸拷貝(深拷貝)
在我們封裝一個自己的深拷貝工具之前,我們需要先跳過一些可能的拷貝錯誤認知 —— “不要單純的認為,只要是拷貝嵌套對象就會產生深拷貝問題”。
(1)就算是拷貝嵌套對象,如果操作的只是基本數據類型,不會有影響的。
let data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } } let newData = Object.assign({}, data); newData.title = 'fuck' console.log(data.title, newData.title) // 123 fuck
(2)就算是嵌套對象,如果只拷貝其中一層對象,是不會產生問題的。
let data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } } let newData = Object.assign({}, data.item); newData.id = 1; console.log(data.item.id, newData.id) // 0 1
有了以上兩個認知,實際上我們可以實現手動深拷貝了。
let data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } } // 創建一個空對象 let newData = {} // 基本數據類型,可以直接拿 newData.title = data.title // data.item對象,其實可以使用 Object.assign 來拿 // newData.item = Object.assign({}, data.item) // 但為了體現手動深拷貝細節,還是一步一步來吧。 // 通過對data.item拆解,item對象被轉換為一個個的基本數據類型直接賦值給newData.item的每一項 // 如果 item 還有對象的話,那就只能依樣畫葫蘆繼續拆解。直到變成基本數據類型為止。 newData.item = {} newData.item.id = data.item.id newData.item.book = data.item.book newData.item.avaliable = data.item.avaliable newData.item.id = 1 console.log(data.item.id, newData.item.id) // 0 1
通過對data.item拆解,item對象就轉換為一個個的基本數據類型了,所以可以直接賦值給newData.item的每一項了。
如果 item 還有對象的話,那就只能依樣畫葫蘆繼續拆解。直到變成基本數據類型為止。
知道了如何手動深拷貝,現在我們把他轉換為自動化深拷貝,常見的的實現方式是遞歸拷貝了:
// 遞歸深拷貝
var deepExtend = function(out) { out = out || {}; for (var i = 1; i < arguments.length; i++) { var obj = arguments[i]; if (!obj) continue; for (var key in obj) { if (obj.hasOwnProperty(key)) { if (typeof obj[key] === 'object') out[key] = deepExtend(out[key], obj[key]); else out[key] = obj[key]; } } } return out; };
// demo let data = { title: '123', item: { id: 0, book: 'learn redux', avaliable: false } } var newData = deepExtend({}, data); newData.item.id = 1 console.log(data.item.id, newData.item.id) // 0 1

