js实现深拷贝
浅拷贝
当把数组或对象简单赋值给其他变量的时候,实际上进行的是浅拷贝,浅拷贝是拷贝引用,只是将拷贝后的引用指向同一个对象实例,彼此间的操作还会互相影响。
1. 使用 for in 遍历
简单的拷贝对象的属性,如果属性是基础类型则没有问题,但是对象是对象类型(数组,函数)则只是拷贝的是引用,改变原始的属性,拷贝出来的属性也会跟着改变
function shallowCopy(source){
var target=source instanceof Array ? [] : {
};
for(var i in source){
// 使用 hasOwnProperty 方法判断属性是否存在
if(source.hasOwnProperty(i)){
target[i]=source[i];
}
}
return target;
}
// 测试
var obj={
a:1,
b:[1,2,3],
c:function(){
console.log('i am c')}
}
var tar=shallowCopy(obj)
tar.c() // "i am c"
obj.a=5
obj.a // 5
tar.a // 1
obj.b[0]=10
obj.b // [10, 2, 3]
tar.b // [10, 2, 3]
var arr=[1,2,[4,5,6]]
var newArr=shallowCopy(arr)
newArr // [1, 2, [4,5,6]]
arr[0]=10
arr // [10, 2, [4,5,6]]
newArr // [1, 2, [4,5,6]]
arr[2][0]=10
arr // [1, 2, [10,5,6]]
newArr // [1, 2, [10,5,6]]
2.0bject . assign
Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。也就是说,如果对象的属性值为简单类型(如string, number),通过Object.assign({},srcObj);得到的新对象为深拷贝;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝的。
var obj={
a:1,b:[1,2,3],c:function(){
console.log('i am c')}}
var tar={
};
Object.assign(tar,obj);
//测试
obj.b[2]=1
console.log(obj.b)//[ 1, 2, 1 ]
console.log(tar.b)//[ 1, 2, 1 ]
3. 数组方法实现数组浅拷贝
- Array.prototype.slice
var arr=[1,2,[3,4]];
var newArr=arr.slice(0);
- Array.prototype.concat
var arr=[1,2,[3,4]];
var newArr=arr.concat();
- Array.from
var arr=[1,2,[3,4]];
var newArr= Array.from(arr);
console.log(newArr)//[ 1, 2, [ 3, 4 ] ]
深拷贝
在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,拷贝后的对象与原来的对象完全隔离,互不影响。
1. 对象的序列化与反序列化
let obj = {
name: 'aaa', age: 30, action: function () {
} }
console.log(JSON.stringify(obj)) //转json字符串
let str = '{"name":"aaa","age":30}'
let obj2 = JSON.parse(JSON.stringify(obj)) //把json字符串转为json对象
obj2.name = "bbb";
console.log(obj, obj2)
存在的坑
- 如果对象中有函数有方法就不能实现拷贝了
- 会抛弃对象的constructor,所有的构造函数会指向Object
- 对象有循环引用,会报错
// 构造函数
function person(pname) {
this.name = pname;
}
const Messi = new person('Messi');
// 函数
function say() {
console.log('hi');
};
const oldObj = {
a: say,
b: new Array(1),
c: new RegExp('ab+c', 'i'),
d: Messi
};
const newObj = JSON.parse(JSON.stringify(oldObj));
// 无法复制函数
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// 稀疏数组复制错误
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// 无法复制正则对象
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// 构造函数指向错误
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]
//对象循环引用会报错
const oldObj = {
};
oldObj.a = oldObj;
const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON
使用递归完成深拷贝
需要递归判断对象的属性是什么数据类型,逐一进行拷贝
思路:
typeof运算符的操作对象是一个函数时,得到的是 “function” 所以在循环里第一个if判断那为false 所以走else分支,在tar[i] = obj[i]这里,函数是进行引用赋值的,如果再造一个相同的函数不是不可以,只是不符合思想罢了,函数占用堆内存,如果可以共用当然是最好的选择。
function deep(dest, ori) {
//dest目标对象 ori源对象
for (var i in ori) {
if (typeof ori[i] === 'object') {
//递归 判断是数组还是对像
dest[i] = (ori[i].constructor === Array) ? [] : {
}; //初始化属性
deep(dest[i], ori[i])
} else {
dest[i] = ori[i]; //非引用属性
}
}
return dest;
}
//测试
function simple(obj) {
var o = {
};
Object.assign(o, obj); //目标对象 源对象
return o;
}
// var a = simple(Animal);
// var b = simple(Animal);
var a = deep({
}, Animal)
var b = deep({
}, Animal)
a.name = "tom";
a.skin.push("white");
// a.say();
// b.say();
console.log(a, b)
但是下面的情况,会循环调用,会陷入一个循环的递归过程,从而导致爆栈,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
解决
function deepCopy(obj, parent = null) {
// 创建一个新对象
let result = {
};
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 && typeof temp=== 'object') {
// 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
result[key] = DeepCopy(temp, {
originalParent: obj,
currentParent: result,
parent: parent
});
} else {
result[key] = temp;
}
}
return result;
}
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj1); //太长了去浏览器试一下吧~
console.log(obj2); //太长了去浏览器试一下吧~