理解
浅拷贝:拷贝的是栈中的地址,修改拷贝后的数据,原始数据也会改变。
深拷贝:拷贝的是堆中的数据,修改拷贝后的数据,对原始数据没有影响。
打个比方:我有一个朋友,朋友穿的一件衣服我很喜欢,然后我就把她衣服拿过来自己穿上了,但是我在衣服上弄上污渍了,还给那个朋友的时候,那个朋友的衣服上有污渍,这就是浅拷贝。
深拷贝:我有一个朋友,朋友穿的一件衣服我很喜欢,然后我找她要了链接自己也买了一件,现在有了两件一样的衣服,当我在我的衣服上弄上污渍了,朋友的那件衣服上还是干净的,这就是深拷贝。
深拷贝浅拷贝主要是针对对象的属性是对象(引用类型)。
浅拷贝
先来看个例子,首先我们先定义一个对象为obj1,在把obj1赋值给新定义的另一个对象obj2,然后在修改obj1里面属性a的值为2,最后打印obj1和obj2,看里面的属性a是多少。
var obj1 = {
a:1,
}
var obj2 = obj1
obj1.a = 2
console.log('obj1',obj1,'obj2',obj2);
根据下面图片我们可以看到,obj1和obj2的属性a的值都为2了,但是看上面的代码,我们是先把obj1赋值给obj2后,在修改obj1的属性值的,这就是我们所说的浅拷贝。
我们再来看个例子,下面是数值型(简单数据类型)的,还是一样,我们先定义一个a,,在把a赋值给新定义的b,然后修改a的值为2,最后打印a和b,看a和b的值多少。
var a = 1;
var b = a;
a = 2;
console.log('a',a);
console.log('b',b);
根据图片我们发现,b的值没有改变,数值型的a修改了值,但是b的值还是1,没有改变,但是这段代码和上面的除了类型不一样,其余的都是差不多的,那为什么对象的属性值一修改,obj1和obj2里的属性值都改变,而数值型的不变呢?
这个就跟简单数据类型和复杂数据类型(引用类型)的存储方式有关
先了解一下简单数据类型和复杂数据类型
简单数据类型(值类型):null String Number undefined Boolean Symbol
复杂数据类型(引用类型):object function Array Date Math RegExp
存储方式区别
简单数据类型值放在栈中,可直接访问和修改,且相互之间不会影响。
(复杂数据类型)引用类型存在堆中,当进行赋值操作时,其实赋值的是地址。
我们可以看下面的这张图,值类型存放在栈中,引用类型存放在堆中,首先就是数值型的代码,先定义一个a放在栈中,在栈中在开一个空间存放b,再把a赋值给b,也就是1,最后修改a的值,是在栈中找到a,然后改为2,当我们打印b的时候,在栈中找到b,值还是为1.
再来看对象,首先先把obj1的内容存放在a中,然后在栈中存放obj1在堆中的地址,在链接到堆中,所以当把obj1赋值给obj2的时候,就是在栈中开一个obj2的地址,存的内容就是obj1在堆中的地址,所以当我们修改obj1的值,再打印obj2的时候,通过栈中的地址找到obj2在堆中的内容,就会发现obj2的值已经改变了。
可以认为赋值就是在拷贝
基本类型赋值时,赋的是数据,所以不存在深拷贝和浅拷贝的问题,可以理解为赋值。
引用类型赋值时,赋的是地址,就是引用类型变量在内存中保存的内容。
深拷贝
在之前已经说了浅拷贝深拷贝主要针对的是引用类型,而引用类型有Array、Object、Function...
接下来就来看看如何实现深拷贝。
深拷贝本质上来说就是在堆中在重新开辟一个地址存放obj2的内容,这样当obj1修改数据后,obj2不会跟着改变了。如下图所示。
1.JSON.parse(JSON.stringify())
JSON.parse(JSON.stringify()) 先把js类型转换为JSON字符串,然后再转为JS对象。
看下面代码,还是先定义一个obj1,在添加属性和方法,然后在把obj1赋值给obj2,之后在修改obj1属性的值,我们再看看obj1和obj2的输出结果是什么。
var obj1 = {
a:1,
say(){
console.log(123);
},
fn:function(){
console.log(111);
},
person:{
name:'张三'
},
reg:/\d/,
data:undefined,
time:new Date(),
err:new Error(),
}
var obj2 = JSON.parse(JSON.stringify(obj1))
// 修改obj1中的属性a为2
obj1.a = 2
// 修改obj1中的属性person对象里的属性名name为李四
obj1.person.name = '李四'
console.log(obj1);
console.log(obj2);
截图如下:
根据上图,我们可以总结 JSON.parse(JSON.stringify()) 几点:
优点:二级以下也可以实现深拷贝
我们发现,我们修改了obj1的person名字为李四,但是在输出里obj2的person里的属性name的值还是张三,这就是我们之前说的深拷贝,拷贝的是堆中的数据,修改拷贝后的数据,对原始数据没有影响。
缺点:
1.函数无法拷贝,function会丢失对应的属性
2.正则无法拷贝,属性值变为空 error也是
3.undefined也无法拷贝,在obj2中根本就没显示
4.Date类型的,属性值变为字符串,即使再次变为对象,属性值还是字符串
2.Object.assign()
Object.assign(目标对象,...源对象)可以有多个源对象。
Object.assign()方法和JSON.parse(JSON.stringify()) 方法的优缺点还挺对应的,你的优点就是我的缺点,我的缺点就是你的优点。
优点 1.可以拷贝函数 2.可以拷贝正则 3.可以拷贝undefine 4.date类型的,属性值不变
缺点:二级以下无法实现深拷贝,只能实现浅拷贝。
var obj1 = {
a:1,
say(){
console.log(123);
},
fn:function(){
console.log(111);
},
person:{
name:'张三'
},
reg:/\d/,
data:undefined,
time:new Date(),
err:new Error(),
}
// 创建一个obj2为空对象
var obj2 = {}
// Object.assign(目标对象,...源对象)可以有多个源对象
Object.assign(obj2,obj1)
// 修改obj1中的属性a为2
obj1.a = 2
// 修改obj1中的属性person对象里的属性名name为李四
obj1.person.name = '李四'
console.log("obj1", obj1);
console.log("obj2", obj2);
截图如下:
我们发现,obj1中的函数、时间、正则表达式等obj2也都拷贝过来了,但是有个问题就是,obj1中的二级person里的属性name修改为李四后,obj2里的person里的属性name值也为李四了,也就是说,Object.assign()一级能实现深拷贝,二级以下无法实现深拷贝,只能实现浅拷贝。
3.递归调用实现深拷贝,解决循环引用问题
我们都发现了,前两个方法虽然都能实现深拷贝,但是也都有一些些小缺点,所以现在第三种方法能完美解决以上缺点。
首先,我们先来了解一下递归,递归一句话:自己调用自己。下面我们会用到一个方法for in循环,根据下图我们发现,obj1的地址为0x1111,obj2的地址为0x2222,遍历循环就是把0x1111中的方法和属性一个一个遍历,然后放入0x2222中,如果有数组,又要开辟一个新的空数组,然后遍历原数组,把原数组里的数据一个一个放入新数组中,直到全部遍历完毕。
代码如下:
var obj1 = {
a:1,
say(){
console.log(123);
},
hobby:['打游戏','看书','睡觉'],
person:{
name:'张三',
age:18,
dog:{
age:2,
name:'小白'
},
},
}
function kb(newobj,obj){
// 遍历 for in可以遍历对象和数组
for(var key in obj){
// console.log(key,obj[key]);
// 判断obj[key]是不是数组类型
if(obj[key] instanceof Array){
// 数组,引用类型
// 声明一个空数组,然后继续拷贝里面的数据
newobj[key] = []
// 再调一次kb
kb(newobj[key],obj[key]) //这里使用递归调用
}
// 如果里面的属性为函数类型,就先赋值,不加这个的话函数就实现不了了
else if(obj[key] instanceof Function){
newobj[key] = obj[key]
}
else if(obj[key] instanceof Object){
// 声明一个空对象,然后继续拷贝里面的数据
newobj[key] = {}
kb(newobj[key],obj[key])
}else{
// 值类型
newobj[key] = obj[key]
}
}
}
// 声明一个新对象存储拷贝后的数据
var obj2 = {}
// 调用函数kb
kb(obj2,obj1)
// 修改obj1中二级person对象属性age为19
obj1.person.age = 19
// 修改obj1中的三级dog中的属性name为小黑
obj1.person.dog.name = '小黑'
// 修改obj1中的hobby中的第0个下标为看直播
obj1.hobby[0] = '看直播'
console.log("obj1",obj1);
console.log("obj2",obj2);
根据截图我们发现,obj2中的数据没有因为obj1的修改而改变,也实现了二级以下深拷贝,函数也出来了。如图所示。
数组拷贝方法
1.数组API slice()实现
slice():数组截取,返回截取后的数组组成的数据,不改变原数组
var a = [1,2,3,4,[11,22]]
var b = a.slice()
// 修改a的下标为4中的下标为0也就是11改为100
a[4][0] = 100
// 修改a的下标为1也就是2的值为111
a[1] = 111
// 打印a,b
console.log(a,b);
根据下图我们发现,slice()方法一级实现深拷贝,二级就是浅拷贝了。
2.数组API concat()
concat():和并数组,返回被合并的数组,不改变原数组。
var a = [1,2,3,4,[11,22]]
var b = a.concat()
// 修改a的下标为4中的下标为0也就是11改为123
a[4][0] = 123
// 修改a的下标为1也就是2的值为222
a[1] = 222
// 打印a,b
console.log("a",a,"b",b);
根据下图我们发现,concat()方法一级实现深拷贝,二级就是浅拷贝了。
3.es6的扩展运算符..
展开运算符允许一个表达式在某处展开。展开运算符在多个参数(用于函数调用)或多个元素(用于数组字面量)或者多个变量(用于解构赋值)的地方可以使用。
var a = [1,2,3,4,[11,22]]
var b = [...a]
// 修改a的下标为4中的下标为0也就是11改为122
a[4][0] = 122
// 修改a的下标为1也就是2的值为234
a[1] = 234
// 打印a,b
console.log("a",a,"b",b);
根据下图我们发现,es6的扩展运算符...和前面两个方法一样,一级实现深拷贝,二级实现浅拷贝。
4.Object.assign()
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
var a = [1,2,3,4,[11,22]]
var b = [];
Object.assign(b,a)
// 修改a的下标为4中的下标为0也就是11改为555
a[4][0] = 555
// 修改a的下标为1也就是2的值为666
a[1] = 666
// 打印a,b
console.log("a",a,"b",b);
根据下图我们发现,Object.assign()和前面的方法都一样,一级实现深拷贝,二级实现浅拷贝。
数组拷贝我们发现只是在第一层是深拷贝,但当嵌套了一个数组的时候,也就是第二层就是浅拷贝了,第二层改变数据也会跟着改变。