本文將提供javascript深拷貝的解決方案,能處理多重嵌套,對數組、函數、Date、RegExp、null、undefined、原型鏈、循環引用等都有進行處理。
相信大家已經瞭解到JavaScript的深拷貝和淺拷貝概念。對於深拷貝,大家常用JSON“序列化+反序列化”的方式來解決,但是
由於JSON.parse(JSON.stringify())進行深拷貝會有如下問題:
對於 JSON.parse(JSON.stringify(data))
- 1.data內若屬性的鍵值爲時間對象,其會轉爲日期字符串形式。
- 2.data內若屬性的鍵值爲RegExp、Error對象,其會轉爲空對象{}
- 3.data內若屬性的鍵值爲undefined、函數或symbol,其會把該屬性丟失。
- 4.data內若屬性的鍵值有NaN、Infinity和-Infinity,其會變成null
- 5.data內若屬性的鍵值由自定義構造函數生成的,轉換後會丟棄對象的constructor。
- 6.data中若存在循環引用的情況也無法正確實現深拷貝。
情況1-5示例:
function Person(name) {
this.name = name;
}
Person.prototype.speak = function(){
console.log('i can speak')
}
var arr1 = {
a:1,
b:undefined,
c:new Date('2019-12-24'),
d:new Person('mily'),
e:new RegExp('\\w+'),
f:NaN,
g:Symbol("foo"),
h:{
total:12,
list:[{name:'小明',age:34},{name:'小紅',age:23}]
}
}
// JSON.parse(JSON.stringify())方式
console.log(JSON.parse(JSON.stringify(arr1))
//結果爲
//{
// a: 1
// c: "2019-12-24T00:00:00.000Z"
// d: {name: "mily"}
// e: {}
// f: null
// h: {total: 12, list: Array(2)}
// }
循環引用示例
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;//循環引用
console.log(JSON.parse(JSON.stringify(arr1)))
直接就報錯了。
所以JSON.parse(JSON.stringify(data))並不是萬能的解決方案。不過JSON.parse(JSON.stringify(data))
簡單粗暴,已經滿足90%的使用場景了。因此 只能通過"遞歸"來解決上述的問題,函數如下:
/**
* isType 判斷數據類型
* @param obj 要檢驗類型的數據,必填
* @param type 要檢驗的數據類型,選填
* @returns {String|Boolean} 返回對應數據格式的字符串,或者與type參數比較的布爾值
*/
const isType = function(obj, type) {
// tostring會返回對應不同的標籤的構造函數
const toString = Object.prototype.toString
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object'
}
if (obj instanceof HTMLElement) {
return type ? type === 'element' : 'element'
}
return type ? type === map[toString.call(obj)] : map[toString.call(obj)]
}
/**
* clone 對象或數組的拷貝函數
* @param obj 要拷貝的數據(對象/數組)
* @returns {Object/Array} 返回對象或數組
*/
const clone = function(obj, parent = null) {
//parent用於遞歸循環引用爆棧處理
// 創建一個新對象
let result = new obj.constructor() //保持繼承鏈
let keys = Object.keys(obj),
key = null,
temp = null,
_parent = parent
// 該字段若有父級則需要追溯該字段的父級
while (_parent) {
// 如果該字段引用了它的父級則爲循環引用
if (_parent.originalParent === obj) {
return _parent.currentParent // 循環引用直接返回同級的新對象
}
_parent = _parent.parent
}
for (let i = 0; i < keys.length; i++) {
key = keys[i]
temp = obj[key]
if (temp && isType(temp) === 'date') {
result[key] = new Date(temp)
continue
}
if (temp && isType(temp) === 'regExp') {
result[key] = new RegExp(temp)
continue
}
// 若字段的值是一個對象/數組
if (temp && (isType(temp) === 'object' || isType(temp) === 'array')) {
// 遞歸執行深拷貝 將同級的待拷貝對象與新對象傳遞給 parent 方便追溯循環引用
result[key] = clone(temp, {
originalParent: obj,
currentParent: result,
parent: parent
})
} else {
result[key] = temp
}
}
return result
}
還是使用上述例子運行,用戶可以輸出複製前的arr1,和複製後clone(arr1)進行比對,發現數據及格式完全沒有丟失。
console.log(arr1,clone(arr1))
用戶還可以手動驗證:修改複製後的var copydata = clone(arr1)的某些屬性,發現不會修改到arr1。真正實現了深拷貝。
上面的isType()函數還可以用來判斷數據的類型,大家可以放在公共函數內積極使用!
初次練手,如果有錯誤及優化的地方請積極指正。