话不多说,首先让我们先来看看以下这段代码:
let obj = {
num: 1 }
console.log(obj.num)
// 1
let copy = obj
copy.num = 2
console.log(obj.num)
// 2
console.log(obj === copy) // true 完全相等的两个对象
从代码我们可以很清楚的看到obj.num的值发生了变化,但是从表面看,我们似乎并没有对obj进行从操作,为什么会出现这种现象?这就是所谓的浅拷贝引起的。
浅拷贝和深拷贝的理解
首先理解引用类型,引用类型存储地址上存储的是它真正数据存放的地址。
换个通俗一点的说法:比如我们去取快递,如果变量是基本类型的话,那我们就是直接去它给我们的地址去取,然而如果是引用类型的话,我们则是去到他给定的地址后拿到一张小纸条,上面写着的才是我们快递所在的真正位置,只有去到纸片上的地址我们才能成功取到我们的快递。
由于对象类型(object, array…)属于引用类型,它在内存上的存储,存储的是它的引用地址(数据真正的存储位置地址),当我们做浅拷贝的操作时,其实就是将这个引用地址赋给另外一个变量,所以最后他们两者拿到的对象是同一个(相当于拷贝的只是那张小纸条)。而深拷贝则是在内存上另外开辟一个位置存储另外一个对象,不过这个对象跟我们所拷贝的对象的数据是一致的。
实现浅拷贝的常用方法
方法1:通过扩展运算符实现
扩展运算符的方式既可以浅拷贝数组(上面已举例),也可以浅拷贝对象,这里我们再举一个浅拷贝对象的例子:
let obj = {
a:1,b:2,c:{
d:3,e:[1,2]}}
let obj1 = {
...obj}
// 通过扩展运算符浅拷贝,获得对象obj1
console.log(obj === obj1)
// false,obj和obj1分别指向不同的对象
console.log(obj.c === obj1.c)
// true,但是obj的c属性的值和obj1的c属性的值是同一个内存地址
方法2:通过Object.assign方法实现
Object.assign()方法只适用于对象,可以实现对象的合并,语法:
Object.assign(target, source_1, ..., source_n).
Object.assign()方法会将source里面的可枚举属性复制到target,复制的是属性值,如果属性值是一个引用类型,那么复制的是引用地址,因此也属于浅拷贝。举例如下:
let target= {
name: "小明",
}
let obj1 = {
age: 28,
sex: "男",
}
let obj2 = {
friends: ['朋友1','朋友2','朋友3'],
sayHi: function (){
console.log( 'hi' )
},
}
let obj = Object.assign(target,obj1,obj2)
console.log(obj === target) // true,因此可以用变量接收结果,也可以直接使用target
obj1.age = 30 // 把obj1的age属性值改成30
console.log("target",target)
console.log("obj1",obj1)
我们可以看出返回的结果obj和target都指向浅拷贝的新对象,修改obj1的属性age不会影响target的age属性值。
此时给target的friends属性添加一个新的朋友4,操作如下:
target.friends.push(“朋友4”)
console.log(“target”,target)
console.log(“obj2”,obj2)
此时target的friends属性和obj2的friends属性的值指向同一个数组。
实现深拷贝的常用方法
方法1:通过递归复制所有层级实现
这里我们通过封装一个deepClone函数来实现深层次拷贝,该方法适用于对象或数组,代码如下:
let obj = {
name: '小明',
age: 20,
arr: [1, 2],
}
function deepClone(value) {
// 判断传入参数不是对象或数组时直接返回传入的值,不再执行函数
if (typeof value !== 'object' || value == null) {
return value
}
//定义函数的返回值
let result
// 判断传进来的数据类型数组还是对象,对应创建新的空数组或对象
if (value instanceof Array) {
result = []
} else {
result = {
}
}
// 循环遍历拷贝
for (let key in value) {
//函数递归实现深层拷贝
result[key] = deepClone(value[key])
}
// 将拷贝的结果返回出去
return result
}
let newObj = deepClone(obj)
obj.arr[0] = 0 // 修改原对象的arr属性对应的数组的元素值
console.log("obj",obj)
console.log("newObj ",newObj )
以下是上面代码的打印结果:
我们可以看到深层递归的方式不会复制引用地址,所以用原对象obj修改其arr属性对应的数组的元素,并不会影响新的对象newObj。
方法2:通过JSON对象的stringify和parse方法实现
上面我们讲解深拷贝概念时用过该方法深拷贝数组,这里我们举例来深拷贝对象:
let obj = {
name: '小明',
age: 20,
arr: [1, 2],
}
let obj1= JSON.parse( JSON.stringify(obj) )
console.log(obj.arr === obj1.arr)
// false,此时obj的arr属性和obj1的arr属性值不是同一个数组
通过代码我们可以发现,JSON.stringify()方法会把obj先转化为字符串,字符串就已经不代表任何空间地址了,就是单纯的字符串,而JSON.parse()方法把字符串解析成新对象,对象的每个层级都会在堆内存中开辟新空间。
总结
JS的浅拷贝与深拷贝主要是作用于多层级数组或对象中。浅拷贝是只复制创建数组或对象的第一层,其他层级和原数组或对象拥有相同地址值,因此修改浅拷贝的数组或对象的深层的数值就会影响原数组或对象的值。而深拷贝则是拷贝一个全新的数组或对象,每一个层级都在堆内存中开辟了新的空间,和原数组或对象相互不影响。