大家可以查到很多的关于深浅复制的文章,有很多都写得很好,我这里就对自己学习到的做一做总结,争取以实例来详细说明JS复制由浅入深复制的整个过程。
查阅了很多的资料,也看到了很多的复制方法,能够完成复制的深度大概可以由下面的这幅图来直观的看出。
方法 | 基本类型 | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|
.... | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
.... | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
..... | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
下面的对象obj
为复制目标,isObject
是工具方法:
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
var h = Symbol('我是一个Symbol');
var obj = {
a: "hello",
a1: null,
a2: undefined,
b:{
a: "world",
b: 21
},
c:["Bob", "Tom", "Jenny"],
d:function() {
alert("hello world");
},
e: new Date('2012-12-12'),
f: new RegExp('\.'),
g: new Error('error')
};
obj[h] = 'symbol';
/*
这里把复制的方法加进来
*/
var cloneObj = deepClone(obj);
obj.a='helloForin';
obj.c[0]='chaichai';
obj.b.a='worldForin';
console.log(obj);
console.log(cloneObj);
上面是测试demo的模板,每次只需把深浅复制的方法定义在注释的地方即可运行查看结果。
1.浅复制
浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。
1.1 Array的slice和concat方法
Array的slice和concat方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法被我归为浅复制
var array = [1, [1,2,3], {name:"array"}];
var array_concat = array.concat();
var array_slice = array.slice(0);
//改变array_concat中数组元素的值
array_concat[1][0] = 5;
console.log(array[1]); //[5,2,3]
console.log(array_slice[1]); //[5,2,3]
//改变array_slice中对象元素的值
array_slice[2].name = "array_slice";
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice
1.2 Object.assign()
var obj = { a: {a: "hello", b: 21} };
var initalObj = Object.assign({}, obj);
initalObj.a.a = "changed";
console.log(obj.a.a); // "changed"
2.深复制
2.1深复制基本功
2.1.1迭代递归法
2.1.1.1 for...in基本功
// 迭代递归法:深拷贝对象与数组
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
}
return cloneObj
}
运行结果:
对比复制前后的对象,可以发现该方法:
方法 | 基本类型(除null、undefined) | null | undefined | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
for...in基本功 | yes | yes | yes | no | yes | yes | no | no | no | no | ? | ? |
2.1.1.2 Reflect基本功
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(cloneObj).forEach(key => {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return cloneObj
}
运行结果:
对比复制前后的对象,可以发现该方法:
方法 | 基本类型(除null、undefined) | null | undefined | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Reflect基本功 | yes | yes | yes | no | yes | yes | no | no | no | yes | ? | ? |
2.1.1.3 序列化反序列化基本功
function deepClone(obj){
let _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone
}
运行结果:
对比复制前后的对象,可以发现该方法:
方法 | 基本类型(除null、undefined) | null | undefined | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
序列化反序列化基本功 | yes | no | no | no | yes | yes | no | no | no | no | ? | ? |
2.2深复制内功
2.2.1 lodash中深拷贝内功
调用lodash中的cloneDeep
方法:let result = _.cloneDeep(obj)
,
var cloneObj = _.cloneDeep(obj);
obj.a='helloForin';
obj.c[0]='chaichai';
obj.b.a='worldForin';
obj.d='fff'
console.log('func相同么? ' + obj.d == cloneObj.d);
console.log('Error相同么? ' + obj.g == cloneObj.g);
console.log(obj);
console.log(cloneObj);
运行结果
可以发现:
方法 | 基本类型(除null、undefined) | null | undefined | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
lodash中深拷贝内功 | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | ? | ? |
2.2.2 jQuery.extend()内功
调用lodash中的cloneDeep
方法:var cloneObj = $.extend(true,cloneObj, obj);
var cloneObj = $.extend(true,cloneObj, obj);
obj.a='helloForin';
obj.c[0]='chaichai';
obj.b.a='worldForin';
console.log('func相同么? ' + obj.d == cloneObj.d);
console.log('Error相同么? ' + obj.g == cloneObj.g);
obj.d='fff'
console.log(obj);
console.log(cloneObj);
运行结果:
对比结果,可以得到:
方法 | 基本类型(除null、undefined) | null | undefined | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
jQuery.extend()内功 | yes | yes | no | yes | yes | yes | yes | yes | yes | no | ? | ? |
2.3 循环引用
若复制的源对象中有一个属性引用该源对象obj.loopObj=obj;
,那么经测试可以发现以上五种方法只有lodash中深拷贝内功可以解决。
下面给出jQuery.extend()内功在这种情况下的运行结果:
lodash中深拷贝内功在这种情况成功复制:
查看lodash的源码可以发现,这种方法可以实现的原因是:lodash 使用的是栈把对象存储起来了,如果有环对象,就会从栈里检测到,从而直接返回结果,悬崖勒马。这种算法思想来源于 HTML5 规范定义的结构化克隆算法,它同时也解释了为什么 lodash 不对 Error 和 Function 类型进行拷贝。
2.3.1 进阶
以for...in
为例,查看lodash的源码之后,知道原理就可以对for...in
基本功进行改造,利用哈希表存储已拷贝过的对象用来判断:
function deepClone(obj, hash = new WeakMap()) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
if (hash.has(obj)) return hash.get(obj);
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
hash.set(obj, cloneObj)
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
运行结果:
2.4 原型属性的复制
这部分待研究,可以知道的是for...in
是可以追踪到原型上面的属性的。
2.5 总结
方法 | 基本类型(除null、undefined) | null | undefined | Function | Array | Object | Date | Error | RegExp | Symbol 键 | 循环引用 | 原型链上的属性 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
lodash中深拷贝内功 | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | ? |
序列化反序列化基本功 | yes | no | no | no | yes | yes | no | no | no | no | no | ? |
Reflect基本功 | yes | yes | yes | no | yes | yes | no | no | no | yes | no | ? |
for...in基本功 | yes | yes | yes | no | yes | yes | no | no | no | no | no | ? |
jQuery.extend()内功 | yes | yes | no | yes | yes | yes | yes | yes | yes | no | ? | ? |