1. 概念
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
const a1 = { b: { c: {} } };
var a2 = shallowClone(a1); // 浅拷贝方法
a2.b.c === a1.b.c // true 新旧对象还是共享同一块内存
var a3 = deepClone(a3); // 深拷贝方法
a3.b.c === a1.b.c // false 新对象跟原对象不共享内存
总之,浅拷贝前后对象的基本数据类型互不影响,引用数据类型由于共享内存互相影响。
深拷贝会直接在堆中创建一块新的内存,拷贝后两个对象完全互不影响。
2. 浅拷贝实现方式
2.1 Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: { name: "kobe", age: 41 }, sports: 'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1);
// { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2.2 展开运算符...
es6里常用的新特性
let obj1 = { name: 'Kobe', address: { x: 100, y: 100 } }
let obj2 = { ...obj1 }
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2', obj2)
// obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
2.3 Array.prototype.concat()
concat()不传参也能实现浅拷贝
let arr = [1, 3, {username : 'kobe'}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
2.4 Array.prototype.slice()
和concat()差不多意思
let arr = [1, 3, {username : 'kobe'}];
let arr2 = arr.slice();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
2.5 函数库lodash自带._clone方法
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
3. 深拷贝实现方式
3.1 JSON.parse(JSON.stringify())
首先利用JSON.stringify()将对象转换为JSON字符串,再用JSON.parse()将字符串解析成对象。这样就相当于创建了一个新的对象,而且新对象会在栈中开辟新的地址。
let arr = [1, 3, {username : 'kobe'}];
let arr2 = JSON.parse(JSON.stringify(arr));
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'kobe' } ]
但是这种方法有它的弊端:
let person = {
name: "张三",
hobbies: ["吃饭", "睡觉", "打豆豆"],
date: new Date,
fuc: () => { },
reg: /w/
}
const person1 = JSON.parse(JSON.stringify(person));
person1.name = '李四'
person1.hobbies[0] = '打球'
console.log(person);
//结果有点长,复制运行一下看结果
结果中date属性从一个对象变成了一个字符串,fuc属性消失了,reg属性变成了空对象。所以说这种方式也是有弊端的:
- undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略
- Date对象会被当作字符串处理
- NaN和Infinity会被当作null
- Map,Set等对象中只能序列化可枚举的属性
- 对包含循环引用的对象执行会报错
因此最好还是用手写的递归浅拷贝方法。
3.2 函数库lodash自带.cloneDeep方法
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
4.手写深拷贝和浅拷贝
4.1 手写浅拷贝
浅拷贝的手写还是比较好理解的,创建个空对象用来克隆,通过for...in遍历obj的键,如果obj自身有这个属性就复制过去。
注意for...in循环会把原型上的属性也给遍历了,所以如果用for...in要加hasOwnProperty()
用new obj.constructor()是为了包含obj是数组/函数等情况
const shallowCopy = (obj) =>{
let res = new obj.constructor();
for(let i of Object.keys(obj)){
res[i] = obj[i];
}
return res;
}
4.2 手写深拷贝
深拷贝需要考虑更多的情况,然后实现浅拷贝的递归即可。
const deepCopy = (obj) =>{
if(obj === null) return null;
if(obj instanceof Date) return new Date(obj);
if(obj instanceof RegExp) return new RegExp(obj);
if(typeof obj !== 'object') return obj;
let res = new obj.constructor();
for(let i of Object.keys(obj)){
res[i] = deepCopy(obj[i]);
}
return res;
}
但是如果深拷贝存在循环引用的情况呢?比如最后来一句obj.aaa = obj,那么需要通过WeakMap去进行处理:
WeakMap有以下优点:WeakMap对象是key=>value形式,不会重复记录;WeakMap是弱引用,如果不再使用,空间会直接释放。
let deepCopy = function(obj , hash = new WeakMap()){
if(obj === null) return obj;
if(obj instanceof Date) return new Date(obj);
if(obj instanceof RegExp) return new RegExp(obj);
if(typeof obj !== 'object') return obj;
// 关键语句 判断属性是否已经存在了
if(hash.has(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
hash.set(obj , cloneObj);
for(let i of Object.keys(obj)){
cloneObj[i] = deepCopy(obj[i] , hash);
}
return cloneObj;
}