一、什么是浅拷贝和深拷贝
浅拷贝
- 对于基本数据类型的成员变量,浅拷贝直接进行值传递,也就是将属性值复制了一份给新的成员变量
- 对于引用数据类型的成员变量,比如成员变量是数组、某个类的对象等,浅拷贝就是引用的传递,也就是将成员变量的引用(内存地址)复制了一份给新的成员变量,他们指向的是同一个事例。在一个对象修改成员变量的值,会影响到另一个对象中成员变量的值。
- 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象
深拷贝
- 对于基本数据类型,深拷贝复制所有基本数据类型的成员变量的值
- 对于引用数据类型的成员变量,深拷贝申请新的存储空间,并复制该引用对象所引用的对象,也就是将整个对象复制下来。所以在一个对象修改成员变量的值,不会影响到另一个对象成员变量的值。
- 在改变新的数组(对象)的时候,不改变原数组(对象)
二、浅拷贝的实现方法
1、Object.assign
Object.assign 是 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
const obj = {};
const source = {
name: 'nordon',
info: {
age: 18
}
};
Object.assign(obj, source);
console.log(obj); // {name: 'nordon', info: {…}}
如此很便捷的完成了浅拷贝,接下来可以修改一下数据
source.info.age = 20;
console.log(obj);
console.log(source);
修改之后,obj和source的age都变成了20,满足浅拷贝定义
注意点:
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性。
const obj = {};
const source = {
name: 'nordon',
symbol: Symbol()
};
Object.defineProperty(source, 'innumerable' ,{
value:'innumerable',
enumerable:false
});
Object.assign(obj, source);
console.log(obj); // {name: 'nordon', symbol: Symbol()}
常见的继承属性toString等也并没有被Object.assign浅拷贝过来
2、扩展运算符
使用扩展运算符也可以完成浅拷贝
const source = {
name: 'nordon',
info: {
age: 18
}
};
const obj = {...source};
Object.assign(obj, source);
source.info.age = 20;
console.log(obj);
console.log(source);
上述代码和使用Object.assign功能相同,其注意事项也相同,两者在使用上基本是可以相互转换
3、Array.prototype.concat
数组的 concat 方法其实也是浅拷贝,使用场景比较少,使用concat连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组
const arr = [1, 2, {name: 'nordon'}];
const newArr = arr.concat();
newArr[2].name = 'wy';
console.log(arr);
console.log(newArr);
会发现arr和newArr索引2的数据都由**{name: 'nordon'}变成了{name: 'wy'}**
4、Array.prototype.slice
数组的 slice 方法其实也是浅拷贝,使用场景比较少,同cancat
const arr = [1, 2, {name: 'nordon'}];
const newArr = arr.slice();
newArr[2].name = 'wy';
三、深拷贝的实现方法
1、使用JSON转换
function deepClone(target) {
//通过数据创建JSON格式的字符串
let str = JSON.stringify(target);
//将JSON字符串转化为JS数据
let data = JSON.parse(str);
return data;
}
缺点:
- JSON转换不能克隆方法;因为
JSON格式字符串不支持Function,在序列化的时候会自动删除; - JSON转换不能克隆循环引用,如: obj.b.push(obj.c); obj.c.j = obj.b;
- 会忽略
undefined和symbol;
2、Object.assign()拷贝
es6新增的方法,可用于对象合并,将源对象的所有可枚举属性,复制到目标对象上。
复制代码var data = {
a: "123",
b: 123,
c: true,
d: [43, 2],
e: undefined,
f: null,
g: function() { console.log("g"); },
h: new Set([3, 2, null]),
i: Symbol("fsd"),
k: new Map([ ["name", "张三"], ["title", "Author"] ])
};
var newData = Object.assign({},data)
console.log(newData)
执行结果
可以看到这个API可以将源对象上的全部数据类型属性值完全复制到一个新的对象上,这难道就是我们所寻找的最完美的深拷贝方式了吗?答案是否,只能说是部分深拷贝,或者说就是浅拷贝,为什么这么说呢,接着往下看。
复制代码var test = { name: '张三' }
var data = {
a: 123,
b: test
}
var newData = Object.assign({},data)
console.log(newData)
// { a: 123, b: { name: '张三' }}
test.age = 18
console.log(newData)
// { a: 123, b: { name: '张三', age: 18 }}
结果很明显,这种方式的拷贝,如果源目标对象中某个属性值是对另一个对象的引用,那么这个属性的拷贝仍然是对引用的拷贝,也就是说当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。
3、递归实现
function DeepCopy(obj) {
// Hash表 记录所有的对象引用关系
let map = new WeakMap();
function dp(obj) {
let result = null;
let keys = null,
key = null,
temp = null,
existObj = null;
existObj = map.get(obj);
// 如果这个对象已被记录则直接返回
if (existObj) {
return existObj;
}
keys = Object.keys(obj);
result = {};
// 记录当前对象
map.set(obj,result);
for (let i = 0; i < keys.length; i++) {
key = keys[i];
temp = obj[key];
// 如果字段的值也是一个对象则递归复制
if (temp && typeof temp === 'object') {
result[key] = dp(temp);
} else {
// 否则直接赋值给新对象
result[key] = temp;
}
}
return result;
}
return dp(obj);
}
4、lodash函数库实现深拷贝
lodash很热门的函数库,提供了 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
三、总结
其实了解了以上的方式就已经非常够用了;重点记住,在日常生产环境当中,使用完美方案—lodash.cloneDeep,掌握常用方法,能够解决问题就好。