在前端面试当中,经常会被问到浅拷贝与深拷贝的问题,这主要是考察面试者对基本数据类型和引用数据类型的理解,今天我们就通过本篇帮助大家详细理解浅拷贝和深拷贝的概念以及实现的几种方式。
一、认识浅拷贝和深拷贝
赋值不属于拷贝
首先,大家需要区分,赋值不属于拷贝:
let arr = [1,2,3]
let arr1 = arr
// 这里仅仅是把数组的内存地址赋值给arr1,这里不叫拷贝
概念
浅拷贝与深拷贝主要是作用于多层级数组或对象时存在的情况,多层级数组及对象举例如下:
let arr = [1,2,[3,4],{n:1}] // 多层级数组,数组里还有数组或对象
let obj = {a:1,b:2,c:{d:3,e:[1,2]}} // 多层级对象,对象里还有数组或对象
(1)浅拷贝:指只对对象或数组的第一层进行复制,其他层级复制的是所存储的内存地址。举例如下:
let arr = [2, 3, [4, 6]]
let arr1 = [...arr] // 这里我们运用扩展运算符浅拷贝了数组arr
console.log(arr === arr1)
// false,可以看到浅拷贝的数组arr1和arr指向的是不同的内存地址
arr[0] = 0 // 这里改动原数组第一个元素
console.log(arr) // [0,3,[4,6]],原数组发生了变化
console.log(arr1) // [2,3,[4,6]],新数组无变化
console.log(arr[2] === arr1[2])
// true,但是它们的第三个元素[4,6],指向的都是同一个数组
// 这里我们修改原数组的第三个元素[4,6]的第一个元素,把4改为1
arr[2][0] = 1
console.log(arr1) // [2, 3, [1, 6]],此时打印新数组,发现它也发生了改变
通过上例可以看出浅拷贝虽然复制出了一个新的数组,但是当数组的元素为引用数据类型时,浅拷贝只拷贝了地址,通过原数组改动这个地址指向的数组,新数组同样也会发生变化。
(2)深拷贝:会构造一个新的复合数组或对象,遇到引用所指向的引用数据类型会继续执行拷贝。用于解决浅拷贝只能拷贝一层的情况。举例如下:
let arr = [2, 3, [4, 6]]
let arr1 = JSON.parse( JSON.stringify(arr) )
// 通过数组转字符串再字符串转数组的方法进行了深拷贝
console.log(arr === arr1)
// false,可以看到深拷贝的数组arr1和arr指向的是不同的内存地址
console.log(arr[2] === arr1[2])
// false,即使是数组里第二层级的数组也是不相同
通过上例可以看出深拷贝是每一个层级都在堆内存中开辟了新的空间,是拷贝了一个全新的数组或对象,不会受原数组或原对象的影响。
二、实现浅拷贝的常用方法
方法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的浅拷贝与深拷贝主要是作用于多层级数组或对象中。浅拷贝是只复制创建数组或对象的第一层,其他层级和原数组或对象拥有相同地址值,因此修改浅拷贝的数组或对象的深层的数值就会影响原数组或对象的值。而深拷贝则是拷贝一个全新的数组或对象,每一个层级都在堆内存中开辟了新的空间,和原数组或对象相互不影响。