前言
js
let a = 1
let b = a
let obj = {
age: 18
}
let obj2 = obj
把 a 的值赋值给 b ,这个是一种拷贝,但是把 obj 的值赋给另一个对象 obj2 ,它就不是拷贝,这是为啥呢,今天我们来深入探讨 js 中的 拷贝 --- 分为浅拷贝和深拷贝,由于原始类型在任何语言层面操作都是值复制,没有共享问题,也就没有"深浅"维度;所以在这里,拷贝只针对引用类型,基于原对象,拷贝得到一个新对象,这也解释了为什么上面对象赋值不算拷贝。
浅拷贝
1、[].slice(0) ---截取数组
[].slice(0) 就是一种浅拷贝,我们来看看一下浅拷贝都有些什么特征
js
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0)
arr2.splice(2, 1)
console.log(arr);

看输出结果,它符合基于原对象,拷贝得到一个新对象,新对象改动不影响旧对象,但是这样也解释不了为啥叫浅拷贝呀,别急,我们再来看一段代码:
js
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0)
arr[4].age = 20
console.log(arr2);

我们可以看到,旧对象里面的子对象在拷贝过后改动会影响新对象里面的子对象,只是因为所有的引用类型都会在堆里放一个引用地址,我们拷贝时新对象里面的子对象是 copy 了一个引用地址,后续子对象里面的值更改过后,新对象也会跟着变,如下图所示:

所以我们可以知道 浅拷贝 --- 新对象受原对象的影响(只拷贝了原对象的第一层,里面的子对象拷贝的还是引用地址)。实现浅拷贝的方法有五种,分别是 [].slice(0) 、[...arr] 、 arr1.concat(arr2) 、 arr.toReversed().reverse()、 Object.assign({}, obj) 。
2、 [...arr] ---解构数组
js
const a = [1, 2, {age: 18}]
let c = [...a]
c.splice(1, 1)
a[2].age = 19
console.log(a);
console.log(c);

3、 arr1.concat(arr2) --- 合并数组
js
const a = [1, 2, 3, {age: 18}]
const b = [4, 5]
const c = a.concat(b)
console.log(c, a);
const d = [].concat(a)
a[3].age = 20
console.log(d);

4、 arr.toReversed().reverse() --- 反转数组
.toReversed() 得到一个反转的新数组,.reverse()方法会反转原数组,配合起来就能实现浅拷贝:
js
const arr = [1, 2, 3, {age: 18}]
const arr2 = arr.toReversed().reverse()
arr2.splice(1, 1)
arr[3].age = 20
console.log(arr2);
console.log(arr);

5、 Object.assign({}, obj) --- 合并对象
它和合并数组不一样,它把后面对象里的值放入前面的对象里面,覆盖前面的对象里的值,返回一个新对象,后面对象不变,所以在这里实现拷贝效果都是在前面放一个空对象
js
const obj = {
name: '俊杰',
like: ['泡脚']
}
const newObj = Object.assign({}, obj)
obj.like[0] = '台球'
console.log(newObj);

手搓浅拷贝
js
const obj = {
name: '俊杰',
age: 18,
like: {
n: '洗脚',
m: '台球'
}
}
function shallowCopy(obj) {
let o = {}
// 遍历原对象,将原对象中的 key,value 都存到新对象中一份
for (let key in obj) { // key 为形参
if (obj.hasOwnProperty(key)) { // 判断当前的 key 是否是 obj 显示属性
o[key] = obj[key] // o.name = obj[name] = '俊杰'
}
}
return o
}
const newObj = shallowCopy(obj)
obj.like.m = '篮球'
console.log(newObj);

深拷贝
顾名思义:层层拷贝,新对象不受原对象修改的影响,深拷贝有两种方法 structuredClone() 和 JSON.parse(JSON.stringify(obj))
1、structuredClone()
js
const obj = {
name: '俊杰',
age: 18,
like: {
n: '洗脚',
m: '台球'
},
a: 123n,
}
const newObj = structuredClone(obj)
obj.like.m = '蓝球'
console.log(newObj);

它可以把子对象里面的东西全给 copy 出来,但是他有一个缺陷,就是无法拷贝函数和 Symbol。
2、 JSON.parse(JSON.stringify(obj))
js
const obj = {
name: '俊杰',
age: 18,
like: {
n: '洗脚',
m: '台球'
}
}
const oo = JSON.parse(JSON.stringify(obj))
obj.like.m = '篮球'
console.log(oo);

JSON.stringify(obj) 这是将里面的对象变为字符串,JSON.parse(str) 则是将里面的字符串变为对象,所以合起来实现了深拷贝,同样的他也有缺陷,无法处理 bigint, undefined, NaN, Infinity, function,Symbol。
手搓深拷贝
js
const obj = {
name: '俊杰',
age: 18,
like: {
n: '洗脚',
m: '台球'
}
}
function deepClone(obj) {
let o = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 判断里面是否有引用类型例如对象,数组(函数除外),null也要除外
if (typeof(obj[key]) == 'object' && obj[key] !== null) {
// 如果是引用类型就再执行一次拷贝
const childObj = deepClone(obj[key])
o[key] = childObj
} else {
o[key] = obj[key]
}
}
}
return o
}
const newObj = deepClone(obj)
obj.like.m = '篮球'
console.log(newObj);
运用递归思想,进行层层拷贝。
结语
拷贝,看似只是"复制"二字,实则藏着 JS 内存模型的全部秘密。
浅拷贝,把引用地址递出去,像递名片------改的是同一份底稿;
深拷贝,把整座房子连地基都搬过来,从此山水不相逢。
下次再写 let obj2 = obj 时,不妨先问一句:
"我只是想要一把钥匙,还是想要一栋真正属于自己的房子?"
弄懂深浅,才能让你的数据不再"藕断丝连",也让你的代码少一句 console.log 的叹息。