深拷貝(深克隆)
之前寫過一篇關於淺拷貝與深拷貝的文章,文章中提到淺拷貝和深拷貝主要是針對引用數據類型(對象、數組、函數)而言的,因爲對於基礎數據類型(string、number、boolean、null、undefined),不存在淺拷貝這一說,只要複製一份,就是一次深拷貝,即通過複製生成的值與原始值之間沒有聯繫。那麼深拷貝的實現方式有哪些呢?
1.JSON.stringify()、JSON.parse()
最簡單粗暴的方法,先通過JSON.stringify()將數據序列化成JSON字符串,然後在使用JSON.parse()方法將JSON字符串反序列化成js對象。
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
}
var obj2 = JSON.parse(JSON.stringify(obj1))
obj2.a = 'hello'
obj1.a // [1, 2, 3]
obj2.a // 'hello'
使用JSON.stringify()、JSON.parse()方法將obj1深拷貝一份賦值給obj2,當改變obj2中a的值時,obj1中a的值並沒有隨着改變,說明obj1和obj2是兩個完全獨立的對象,而不是指向同一對象的兩個索引。
使用JSON.stringify()、JSON.parse()方法就可以通喫深拷貝了嗎?答案是否定的,使用JSON.stringify()、JSON.parse()方法實現深拷貝會存在以下問題:
1.1、存在循環引用時會報錯
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈'
}
obj1.self = obj1
var obj2 = JSON.parse(JSON.stringify(obj1))
上面obj1的self屬性值指向obj1,因此obj1是一個存在循環引用的對象,因此在使用JSON.stringify()、JSON.parse()方法進行深拷貝時,就會出現上圖中的報錯。
1.2、存在函數時無法深複製成功
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
fn: function() {
console.log('fn')
}
}
var obj2 = JSON.parse(JSON.stringify(obj1))
上面obj1的fn屬性值是一個函數,執行深複製後打印obj2值,會發現obj2中沒有fn屬性。
1.3、存在正則表達式時無法複製成功
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
reg: /^(a-z)*$/
}
var obj2 = JSON.parse(JSON.stringify(obj1))
上面obj1的reg屬性值是一個正則表達式,執行深複製後打印obj2值,會發現obj2中reg屬性值是一個空對象。
1.4、存在時間值時無法複製成功
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
time: new Date()
}
var obj2 = JSON.parse(JSON.stringify(obj1))
上面obj1的time屬性值是一個時間對象,執行深複製後打印obj2值,會發現obj2中time屬性值是一個時間字符串。
1.5、存在symbol值時無法複製成功
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
sym: Symbol()
}
var obj2 = JSON.parse(JSON.stringify(obj1))
上面obj1的sym屬性值是一個Symbol類型值,執行深複製後打印obj2值,會發現obj2中沒有sym屬性。
1.6、存在構造函數或構造函數生成的實例時,複製之後的構造函數或構造函數生成實例的constructor會丟失
function Person(name){
this.name = name
}
var obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
instance: new Person('Ryan')
}
var obj2 = JSON.parse(JSON.stringify(obj1))
上面obj1的instance屬性值是Person的一個實例,執行深複製後打印obj2值,會發現obj2的instance屬性值的構造函數constructor指向Object構造函數而不是Person構造函數。
2.自己實現一個功能比較完整的深拷貝
深拷貝中解決循環引用時調用棧超出最大值的思路就是當拷貝後新生成對象中存在引用自身時,確保該值的指向爲新生成的對象,而不是原對象,從而切斷新生成對象與原對象之間的聯繫,保證循環引用正確,如下圖所示:
解決循環引用常見的有兩種方法:
1、使用weakMap生成hash表來處理循環引用,即在weakMap中以原對象爲key,新生成的對象爲value儲存起來,當存在循環引用時,如果weakMap中存在原對象的值,則直接使用新生成的對象,否則將原對象與新對象以鍵值對的形式儲存起來:
function deepClone(data, hash = new WeakMap()) {
if(data === null) return null
if(typeof data !== 'object'){
return data
}
if(hash.has(data)) return hash.get(data) // 利用hash表處理循環引用
let newData
if (isType(data, 'Array')) { // 處理數組複製
newData = []
} else if(isType(data, 'RegExp')){ // 處理正則表達式
newData = new RegExp(data.source, getRegExpFlag(data))
} else if(isType(data, 'Date')) { // 處理時間對象
newData = new Date(data.getTime())
} else { // 處理對象複製,並保留其原型
let prototype = Object.getPrototypeOf(data)
newData = Object.create(prototype)
}
hash.set(data, newData)
for(let i in data){
if(data.hasOwnProperty(i)){
newData[i] = deepClone(data[i], hash)
}
}
return newData
}
function isType(data, type){
return Object.prototype.toString.call(data) === `[object ${type}]`
}
function getRegExpFlag(data) {
let flag = ''
if(data.global) flag += 'g'
if(data.ignoreCase) flag += 'i'
if(data.multiline) flag += 'm'
return flag
}
2、使用數組來處理循環引用,跟使用weakMap類似,不同的是weakMap使用鍵值對儲存原對象和新生成對象,數組則將原對象與新生成對象組合在一起儲存起來,當循環引用時,在該數組中查詢,如果數組中存在原對象,則直接返回新對象使用,否則將原對象與新生成對象組合起來存放在數組中:
function deepClone(data, arr = []) {
if (data === null) return null
if (typeof data !== 'object') {
return data
}
let target = arr.find(item => item.parent === data)
if (target) return target.child // 利用數組處理循環引用
let newData
if (isType(data, 'Array')) { // 處理數組複製
newData = []
} else if (isType(data, 'RegExp')) { // 處理正則表達式
newData = new RegExp(data.source, getRegExpFlag(data))
} else if (isType(data, 'Date')) { // 處理時間對象
newData = new Date(data.getTime())
} else { // 處理對象複製,並保留其原型
let prototype = Object.getPrototypeOf(data)
newData = Object.create(prototype)
}
arr.push({
parent: data,
child: newData
})
for (let i in data) {
if (data.hasOwnProperty(i)) {
newData[i] = deepClone(data[i], arr)
}
}
return newData
}
測試:
function Person(name) {
this.name = name
}
let obj1 = {
a: [1, 2, 3],
b: '哈哈哈',
fn: function () {
console.log('obj1')
},
reg: /^(a-z)*$/,
time: new Date(),
instance: new Person('Ryan')
}
obj1.self = obj1
let obj2 = deepClone(obj1)