拷贝指的就是将某个变量的值复制给另外一个变量的过程,关于拷贝可以分为浅拷贝与深拷贝。
针对不同的数据类型,浅拷贝与深拷贝会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同。
对于基本数据类型,变量存储的是值本身,
对于引用数据类型,变量存储的是值在内存中的地址,如果有多个变量同时指向同一个内存地址,其中对一个变量的值进行修改以后,其它的变量也会受到影响。
var arr=[1,23,33]
var arr2=arr
arr2[0]=10;
console.log(arr) // [10, 23, 33]
在上面的代码中,我们把arr
赋值给了arr2
,然后修改arr2
的值,但是arr
也受到了影响。
正是由于数据类型的不同,导致在进行浅拷贝与深拷贝的时候首先的效果是不一样的。
基本数据类型不管是浅拷贝还是深拷贝都是对值的本身的拷贝。对拷贝后值的修改不会影响到原始的值。
对于引用数据类型进行浅拷贝,拷贝后的值的修改会影响到原始的值,如果执行的是深拷贝,则拷贝的对象和原始对象之间相互独立,互不影响。
所以,这里我们可以总结出什么是浅拷贝,什么是深拷贝。
浅拷贝:如果一个对象中的属性是基本数据类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,(只拷贝一层)也就是拷贝后的内容与原始内容指向了同一个内存地址。(第一层互不影响,第二层值的修改会影响到原始的值。)
深拷贝:如果一个对象中的属性是基本数据类型,拷贝的也是基本类型的值,如果属性是引用类型,就将其从内存中完整的拷贝一份出来,并且会在堆内存中开辟出一个新的区域存来进行存放,而且拷贝的对象和原始对象之间相互独立,互不影响。
一、浅拷贝及其实现
下面我们先来看一下浅拷贝的内容
var obj = { a: 1, arr: [2, 3], o: { name: "zhangsan" } };
var shallowObj = shallowCopy(obj);
function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
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,值没有收到影响
除了以上方式实现浅拷贝以外,还可以通过ES6
中的Object.assign()
函数来实现,该函数可以将源对象中的可枚举的属性复制到目标对象中。
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 受影响
二、深拷贝及其实现
下面,我们来看一下深拷贝内容
这里,我们可以使用
JSON.parse(JSON.stringify());
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 不受影响
以上通过JSON
对象,虽然能够实现深拷贝,但是还是有一定的问题的。
第一:无法实现对函数的拷贝
第二:如果对象中存在循环引用,会抛出异常。
第三:对象中的构造函数会指向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));
自己模拟实现深拷贝
这里,我们实现一个简单的深拷贝,当然也可以使用第三方库中的方法来实现深拷贝,例如:可以使用jQuery
中的$.extend()
在浅拷贝中,我们通过循环将源对象中属性依次添加到目标对象中,而在深拷贝中,需要考虑对象中的属性是否有嵌套的情况(属性的值是否还是一个对象),如果有嵌套可以通过递归的方式来实现,直到属性为基本类型,也就是说,我们需要将源对象各个属性所包含的对象依次采用递归的方式复制到新对象上。
function clone(target) {
if (typeof target === "object") {
let objTarget = {};
for (const key in target) {
//通过递归完成拷贝
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
};
var result = clone(obj);
console.log(result);
以上就是一个最简单的深拷贝功能,但是在这段代码中我们只考虑了普通的object
,还没有实现数组,所以将上面的代码修改一下,让其能够兼容到数组。
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {};
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],
};
var result = clone(obj);
console.log(result);
在上面的代码中,添加了let objTarget = Array.isArray(target) ? [] : {};
判断target
是否为数组。
下面我们来看一下循环引用的情况:
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {};
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);
在上面的代码中,添加了obj.o=obj
.然后出现了Maximum call stack size exceeded
以上的错误表明了递归进入了死循环导致栈内存溢出。
原因是:对象存在循环引用的情况,也就是对象的属性间接或直接引用了自身的情况。
解决的方法:这里我们可以额外开辟一个存储空间,在这个存储空间中存储当前对象和拷贝对象之间的对应关系。
当需要拷贝当前的对象的时候,先去这个存储空间中进行查找,如果没有拷贝过这个对象,执行拷贝操作。如果已经拷贝过这个对象,直接返回,这样就可以解决循环引用的问题。
let map = new WeakMap();
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {};
// 如果有直接返回
if (map.get(target)) {
return target;
}
//存储当前对象与拷贝对象的对应关系
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);
以上就是一个基本的深拷贝的案例。