在说深拷贝和浅拷贝之前,我们先来了解下js的数据类型。
基本类型:目前ES中有6种类型分别是String、Number、Boolean、Null、Undefined、Symbol,基本类型指的是保存在栈内存中的简单数据段。按值访问,可以直接操作保存在变量中的实际的值。
引用类型:常用的有Array、Object、Function、Regex、Date等,引用类型指的是保存在堆内存中的对象,意思是变量保存的实际上是在栈内存中的一个指针,而这个指针指向内存中的地址。在JavaScript中不允许直接访问内存中的位置,所以在访问和操作引用类型时,是先从栈中去取出对象的地址指针,然后再从堆内存中取得所需的数据进行操作。
那么这两种类型是如何复制呢?
//基本类型复制
let num = 1;
let num2 = num;
num2 = 113;
console.log(num,num2);//1
//引用类型复制
let a = [1,2,{name:'Tom'}];
let b = a;
a[0]=8;
b[2].name ='Lucky';
console.log(a);// [8, 2,{name: "Lucky"}]
console.log(b);// [8, 2,{name: "Lucky"}]
以上例子中,num赋值给num2,是按值传递,将num值复制一份保存在变量num2,这两个值是是独立的,所以两者之间不会建立联系;当复制引用类型值时,是按址传递,复制的其实是对象在栈中的地址指针,由于两个变量指向的都是栈中同一个指针,此时指针指向的是堆内存中同个地址,那么a和b就建立起了联系,所以操作任一变量,两个变量都会同时改变。
可以说深拷贝一定是按值传递,浅拷贝一定有按址传递,且深拷贝是相对引用类型而言的。
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
浅拷贝只在根属性上在堆内存中创建了一个新的的对象,复制了基本类型的值,但是复杂数据类型也就是对象则是拷贝相同的地址,而深拷贝则是对于复杂数据类型在堆内存中开辟了一块内存地址用于存放复制的对象并且把原有的对象复制过来,这2个对象是相互独立的,也就是2个不同的地址。
1.Object.assign()
let obj={};
let newObj= {name:'Tom',age:33 };
Object.assign(obj, newObj);
newObj.name="Lucky";
console.log(obj,newObj);// {name: "Tom", age: 33} {name: "Lucky", age: 33}
// 从控制台的输出,看似改动后互不影响,二者之间没有联系,
// 那是不是Object.assign就实现了深拷贝呢?
// 如果不确定接着看以下例子
let target = {};
let source = { a: { b: 2 } };
Object.assign(target, source);
console.log(target); // { a: { b: 10 } };
source.a.b = 10;
console.log(source); // { a: { b: 10 } };
console.log(target); // { a: { b: 10 } };
// 通过控制台可以发现,打印结果中,改变source里的b属性,target也变为了10
// 拷贝引用类型时,拷贝的是地址指针,证明Object.assign是一个浅拷贝
Object.assign还有一些注意的点是:
let obj1 = {
a:{
b:1
},
sym:Symbol(1)
};
Object.defineProperty(obj1,'innumerable',{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log(obj1); // {a: {b:2}, sym: Symbol(1), innumerable: "不可枚举属性"}
console.log(obj2); // {a: {b:2}, sym: Symbol(1)}
2.Array.prototype.concat和Array.prototype.slice
let arr = ['one', 'two', 'three',{a:1}];
let newArr = arr.concat();
newArr.push('four')
console.log(arr) // ["one", "two", "three", {a:1},]
console.log(newArr) // ["one", "two", "three", {a:1}, "four"]
// 从控制台输出可看出,创建了一个新对象拷贝arr,arr和newArr存放了不同的地址指针,
// 所以newArr添加元素后不会影响原来的对象,那么arr.concat是不是就实现了深拷贝?
// 接着再看下改变数组中的引用类型的数据
newArr[3].a=33;
console.log(arr) // ["one", "two", "three", {a:33}]
console.log(newArr) // ["one", "two", "three", {a:33}, "four"]
// 此时从控制台输出可看出,arr和newArr中的属性a值都改变了,
// 所以Array.prototype.concat是一个浅拷贝
// 区分浅拷贝和深拷贝主要看拷贝对象中的值为引用类型时,拷贝的是地址还是值。
// 同理Array.prototype.slice也是一个浅拷贝
let arr2 = ['one', 'two', 'three',{a:1}];
let newArr2 = arr.slice();
newArr[3].a=4;
console.log(arr2) // ["one", "two", "three", {a:4}]
console.log(newArr2) // ["one", "two", "three", {a:4}]
3.扩展运算符
let obj = {a:1,b:{c:1}}
let obj2 = {...obj};
obj.a=2;
console.log(obj); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2;
console.log(obj); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
以上方法都只能对一维对象或一维数组进行深拷贝,且都有同样的缺陷,对于值是对象的属性无法完全拷贝成2个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符更加方便。
若想要对任意拷贝对象的操作都不影响原对象的话,就需要用到深拷贝,那么下面我们就接着看深拷贝。
1.一个简单的深拷贝
let obj1 = {
a: {
b: 1
},
c: 1
};
let obj2 = {};
obj2.a = {}
obj2.c = obj1.c
obj2.a.b = obj1.a.b;
console.log(obj1); //{a:{b:1},c:1};
console.log(obj2); //{a:{b:1},c:1};
obj1.a.b = 2;
console.log(obj1); //{a:{b:2},c:1};
console.log(obj2); //{a:{b:1},c:1};
2.JSON.stringify
JSON.stringify()是目前前端开发过程中最常用的深拷贝方式,原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象。
var a = {age:1,name:'ccy',info:{address:'wuhan',interest:'playCards'}};
var b = JSON.parse(JSON.stringify(a));
a.info.address = 'shenzhen';
console.log(a.info,b.info);
// {address: "shenzhen", interest: "playCards"}
// {address: "wuhan", interest: "playCards"}
通过JSON.stringify实现深拷贝还有一些注意的点是:
function Obj() {
this.func = function () {
alert(1)
};
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN
this.infinity = Infinity
this.sym = Symbol(1)
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
})
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
控制台输出: