本文将提供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()函数还可以用来判断数据的类型,大家可以放在公共函数内积极使用!
初次练手,如果有错误及优化的地方请积极指正。