什么是拷贝?
拷贝就是将某个变量的值复制给另外一个变量的过程
拷贝分两种:浅拷贝与深拷贝。
针对不同的数据类型,浅拷贝与深拷贝会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同。
基本数据类型:变量存储值本身;
引用数据类型:变量存储的 是值在内存中的地址。栈中存储的是数据堆的地址。如果有多个变量同时指向同一个内存地址,其中对一个变量的值进行修改以后,其它的变量也会受到影响。
var arr=[1,23,33]
var arr2=arr
arr2[0]=10;
console.log(arr) // [10, 23, 33]
所以,正是由于数据类型的不同,导致在进行浅拷贝与深拷贝的时候的效果是不一样的。
浅拷贝
什么是浅拷贝?
如果一个对象中的属性是基本数据类型,拷贝的就是基本类型的值;
如果属性是引用类型,拷贝的就是内存地址,也就是拷贝后的内容与原始内容指向了同一个内存地址,这样拷贝后的值的修改会影响到原始的值。
浅拷贝的实现一:
通过封装函数shallowCopy()实现。我们通过循环将源对象中属性依次添加到目标对象中。
<script>
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var shallowObj = shallowCopy(obj);
// 封装函数
function shallowCopy(obj) {
let newObj = {};
// 遍历obj对象
// 判断属性是否是obj的私有属性
// 若是 将属性赋给新的对象newObj
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = src[key];
}
}
// 最后返回这个新的对象newObj
return newObj;
}
obj.o.name = "lisi";
console.log(shallowObj.o.name); //lisi,值受到了影响
obj.arr[0] = 20;
console.log(shallowObj.arr[0]); //20,值受到了影响
obj.a = 10;
console.log(shallowObj.a); // 1,值没有收到影响
</script>
浅拷贝的实现二:
还可以通过ES6中的Object.assign(armObj,odlObj)函数来实现浅拷贝,该函数可以将源对象中的可枚举的属性复制到目标对象中。
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var result = {};
// 将obj对象拷贝给result对象
Object.assign(result, obj);
console.log(result);
obj.a = 10;
console.log(result.a); // 1,不受影响
obj.arr[0] = 20;
console.log(result.arr[0]); //20 受影响
obj.o.name = "lisi";
console.log(result.o.name); // lisi 受影响
</script>
结果如下:
深拷贝
如果一个对象中的属性是基本数据类型,拷贝的也是基本类型的值;
如果属性是引用类型,就将其从内存中完整的拷贝一份出来,并且会在堆内存中开辟出一个新的区域存来进行存放,而且拷贝的对象和原始对象之间相互独立,互不影响。
深拷贝实现方式一:
可以使用 JSON 实现深拷贝!
JSON.parse(JSON.stringify());
<script>
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var str = JSON.stringify(obj);
var resultObj = JSON.parse(str);
obj.a = 10;
console.log(resultObj.a); // 1 不受影响
obj.arr[0] = 20;
console.log(resultObj.arr[0]); // 2 不受影响
obj.o.name = "lisi";
console.log(resultObj.o.name); // zhangsan 不受影响
</script>
但是这种方法存在一些不足:
(1)、无法实现对函数的拷贝。因为 JSON 格式不支持函数、NaN、underfine 等类型。 (2)、若对象中存在循环引用,会抛出错误。 (3)、对象中的构造函数会指向 object,破坏原型链关系。
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
var obj = {
fn: function () {
console.log("abc");
},
// 属性o的值为某个对象
o: person,
};
var str = JSON.stringify(obj);
var resultObj = JSON.parse(str);
console.log("resultObj=", resultObj); // 这里丢失了fn属性。因为该属性的值为函数
console.log(resultObj.o.constructor); //指向了Object,导致了原型链关系的破坏。
console.log(obj.o.constructor); // 这里指向Person构造函数,没有问题
运行结果如下:
下面我们再来看一下循环引用的情况:
var obj = {
userName: "zhangsan",
};
obj.a = obj;
var result = JSON.parse(JSON.stringify(obj));
深拷贝实现方式二:
在浅拷贝中,我们通过循环将源对象中属性依次添加到目标对象中。
而在深拷贝中,需要考虑对象中的属性是否有嵌套的情况(属性的值是否还是一个对象),如果有嵌套可以通过递归的方式来实现,直到属性为基本类型,也就是说,我们需要将源对象各个属性所包含的对象依次采用递归的方式复制到新对象上。
在这个过程中需要注意两个问题:
-
若拷贝对象是一个数组时,typeof 类型判断出来也是object。因此需要先进行
let objTarget = Array.isArray(target) ? [] : {};判断是否为数组。 -
若对象中出现循环引用情况时,对象存在循环引用的情况,也就是对象的属性间接或直接引用了自身的情况。这样会导致递归进入死循环导致栈内存溢出。然后出现了
Maximum call stack size exceeded
解决:需要额外开辟一个存储空间,在这个存储空间中存储当前对象和拷贝对象之间的对应关系。当需要拷贝当前的对象的时候,先去这个存储空间中进行查找,如果没有拷贝过这个对象,执行拷贝操作。如果已经拷贝过这个对象,直接返回,这样就可以解决循环引用的问题。
<script>
// 开辟一个map存储空间 存储当前对象与拷贝对象之间对应关系
let map = new WeakMap()
function clone(target) {
if (typeof target === 'object') {
// 判断传入的目标对象target 是否为数组
// 若是数组 给objTarget赋为[],否则赋{}
let objTarget = Array.isArray(target) ? [] : {};
// 需要拷贝时,先判断这个map空间查找 是否有这个对象
if (map.get(target)) {
// 表示map空间里面有这个对象 直接将这个对象返回
return target;
}
// 此时表示这个map空间里没有要拷贝的对象
// 没有时,在这个map空间中 存储 当前对象与拷贝对象的对应关系
map.set(target, objTarget)
// 通过循环递归完成深拷贝
for (const key in target) {
objTarget[key] = clone(target[key])
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
//添加数组
arr: [2, 3],
};
obj.o = obj; //构成了循环引用
var result = clone(obj);
console.log(result);
</script>
结果如下:
这样深拷贝就可以实现啦!