前言
在JavaScript中,拷贝是我们日常开发中高频用到的操作,但很多小伙伴会被"浅拷贝"和"深拷贝"搞晕------为什么有时候修改原对象,新对象也跟着变?有时候却完全不受影响?
其实答案很简单:拷贝只针对引用类型(对象、数组等),基本类型(数字、字符串、布尔值)不存在"深浅"一说(赋值就是直接拷贝值)。今天我们就结合具体代码,把浅拷贝和深拷贝讲透。
先明确核心概念
无论是浅拷贝还是深拷贝,核心都是「基于原对象,创建一个新对象」,区别在于:拷贝的"深度"不同,是否会拷贝原对象内部的子引用类型。
重点牢记:引用类型的赋值,赋值的是「内存地址」,不是值本身;而拷贝是创建新对象,只是拷贝的"层数"有差异。
一、浅拷贝:只抄"表面",子对象还是"共用"
浅拷贝的特点:只拷贝原对象的第一层属性。如果原对象的属性值是引用类型(比如子对象、数组),那么拷贝的不是值,而是这个子对象的内存地址------也就是说,原对象和新对象的子引用类型,共用同一个"空间",修改其中一个,另一个会跟着变。
1. 数组浅拷贝:slice() 方法
javascript
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0) // 浅拷贝数组arr(slice不传参、传0效果一致,均拷贝整个数组)
arr[4].age = 20 // 修改原数组中的子对象属性
console.log(arr2); // 输出:['a','b','c','d',{age:20}]
🔍 解析:slice(0) 会创建一个新数组,拷贝原数组的第一层元素。但数组中第4个元素是对象(引用类型),拷贝的是它的地址,所以修改原数组中这个对象的age,新数组arr2中的对应对象也会同步变化。
2. 数组浅拷贝:扩展运算符 [...arr]
javascript
const a = [1, 2, {age: 18}]
let c = [...a] // 用扩展运算符浅拷贝数组a
a[2].age = 19 // 修改原数组中的子对象属性
console.log(c); // 输出:[1,2,{age:19}]
🔍 解析:扩展运算符是最简洁的数组浅拷贝方式之一,原理和slice()一致,只拷贝第一层。子对象依然是共用地址,修改原对象子属性,新对象会受影响。
3. 数组浅拷贝:concat() 方法
javascript
const a = [1, 2, 3, {age: 18}]
const d = [].concat(a) // 用concat()浅拷贝数组a
a[3].age = 20 // 修改原数组中的子对象属性
console.log(d); // 输出:[1,2,3,{age:20}]
🔍 解析:concat() 原本用于拼接数组,当只传入一个原数组时,会返回一个新数组,本质也是浅拷贝,子引用类型依然共用地址。
4. 数组浅拷贝:toReversed().reverse() (特殊小技巧)
javascript
const arr = [1, 2, 3]
const arr2 = arr.toReversed().reverse() // 先反转再反转,得到原数组的浅拷贝
console.log(arr2); // 输出:[1,2,3]
🔍 解析:toReversed() 会返回一个反转后的新数组(不修改原数组),再调用reverse() 反转回来,相当于创建了一个和原数组完全一样的新数组,属于浅拷贝。
5. 对象浅拷贝:Object.assign() 方法
javascript
const obj = {
name: '跳跳虎',
like: ['侦探'] // 子属性是数组(引用类型)
}
const newObj = Object.assign({}, obj) // 浅拷贝obj到新对象
obj.like[0] = '蹦蹦跳跳' // 修改原对象的子数组
console.log(newObj); // 输出:{name: '跳跳虎', like: ['蹦蹦跳跳']}
🔍 解析:Object.assign(target, source) 会将source对象的属性拷贝到target对象,返回target。这里target是空对象,相当于创建了一个新对象,拷贝原对象的第一层属性。子数组like是引用类型,拷贝的是地址,所以修改原对象的like,newObj的like也会变。
6. 手动实现浅拷贝
javascript
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '侦探',
m: '蹦蹦跳跳'
}
}
// 自定义浅拷贝函数
function shallowCopy(obj) {
let o = {} // 创建新对象
// 遍历原对象,拷贝第一层属性(只拷贝自身可枚举属性)
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 排除原型链上的属性
o[key] = obj[key] // 引用类型拷贝地址,基本类型拷贝值
}
}
return o
}
const newObj = shallowCopy(obj)
obj.like.m = '麦芽糖' // 修改原对象的子对象属性
console.log(newObj); // 输出:like.m 也变成了'麦芽糖'
🔍 解析:手动实现的浅拷贝逻辑很简单------创建新对象,遍历原对象的自身属性,将属性值赋值给新对象。和内置方法一样,子引用类型拷贝的是地址,会受原对象影响。
二、深拷贝:层层拷贝,完全"独立"
深拷贝的特点:层层拷贝,直到所有层级的属性都是基本类型。新对象和原对象完全独立,修改原对象的任何属性(包括子对象、子数组),新对象都不会有任何变化------相当于"复制粘贴"了一整个对象,连里面的"小零件"都复制了一份。
1. 深拷贝:structuredClone() 方法
javascript
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '侦探',
m: '蹦蹦跳跳'
},
a: 123n, // BigInt类型
date: new Date(), // Date类型
map: new Map([['key1', 'value1']]), // Map类型
say() {
console.log('hello');
}
}
const newObj = structuredClone(obj) // 深拷贝obj
obj.like.m = '麦芽糖' // 修改原对象的子对象属性
obj.date.setFullYear(2025) // 修改原对象的Date属性
console.log(newObj);
// 输出:like.m 依然是'蹦蹦跳跳',date依然是原日期(不受影响)
// 注意:say()函数被忽略,map被正常拷贝
🔍 解析:structuredClone() 是ES2022新增的深拷贝方法,用法简单,能处理大部分引用类型(对象、数组、Map、Set、Date、RegExp、ArrayBuffer等),支持BigInt类型,且能正确拷贝循环引用(比如obj.self = obj,不会报错)。
注意:structuredClone() 有以下局限:
- 无法拷贝函数(普通函数、箭头函数、类方法均会被忽略);
- 无法拷贝原型链上的属性(只拷贝对象自身属性);
- 无法拷贝Error、Function、GeneratorFunction、AsyncFunction等类型;
- 无法拷贝Symbol类型(会被忽略);
- 无法拷贝DOM节点(会报错)。
2. 深拷贝:JSON.parse(JSON.stringify(obj))
javascript
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '侦探',
m: '蹦蹦跳跳'
},
say() {
console.log('hello');
},
a: undefined, // undefined类型
b: null,
c: NaN, // NaN类型
d: Infinity // 无穷大类型
}
const oo = JSON.parse(JSON.stringify(obj)) // 深拷贝
obj.like.m = '麦芽糖' // 修改原对象
console.log(oo);
// 输出:{name: '跳跳虎', age:18, like:{n:'侦探',m:'蹦蹦跳跳'}, b:null, c:null, d:null}
🔍 解析:这是最常用的"民间深拷贝方法",原理是:先将对象转为JSON字符串(JSON.stringify),再将字符串转回对象(JSON.parse),相当于重新创建了一个完全独立的对象。
注意:这个方法有明显局限,无法处理以下类型,会自动忽略或转换:
-
函数(say()方法被忽略)
-
BigInt(会报错,无法转换)
-
undefined(被忽略)
-
NaN、Infinity(会转为null)
3. 手动实现深拷贝
javascript
const obj = {
name: '跳跳虎',
age: 18,
like: {
n: '侦探',
m: '蹦蹦跳跳'
}
}
// 自定义递归深拷贝函数
function deepClone(obj) {
let o = {} // 创建新对象
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 只拷贝自身属性
// 判断当前属性值是否是引用类型(且不是null,因为null的typeof也是object)
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); // 输出:like.m 依然是'蹦蹦跳跳'(完全独立)
🔍 解析:手动深拷贝的核心是「递归」------遍历对象时,如果遇到引用类型(且不是null),就递归调用自身,对这个子对象也进行深拷贝;如果是基本类型,就直接赋值。这样就能实现"层层拷贝",新对象和原对象完全独立。
补充:这个基础版递归深拷贝,还可以优化(比如处理数组、Map、Set等),但日常开发中,处理普通对象和数组已经足够啦~
三、补充知识点:for...in 遍历与hasOwnProperty()
在上面的拷贝函数中,我们都用到了 for (let key in obj) 和 obj.hasOwnProperty(key),很多小伙伴会疑惑这两个的作用,这里简单补充:
javascript
// 给Object原型添加一个属性(模拟原型链上的属性)
Object.prototype.d = 4
const obj = {
a: 1,
b: 2,
c: 3
}
// for...in 会遍历对象自身的属性 + 原型链上的可枚举属性
for (let key in obj) {
// hasOwnProperty(key):判断key是否是对象自身的属性(排除原型链上的属性)
if (obj.hasOwnProperty(key)) {
console.log(key); // 输出:a, b, c(不会输出d)
}
}
🔍 解析:拷贝时,我们只需要拷贝对象「自身的属性」,不需要拷贝原型链上的属性(比如上面的d属性),所以用hasOwnProperty() 来过滤,避免拷贝多余的属性。
四、浅拷贝vs深拷贝 总结表
| 类型 | 核心特点 | 常用方法 | 注意事项 |
|---|---|---|---|
| 浅拷贝 | 只拷贝第一层,子引用类型共用地址 | slice()、[...arr]、concat()、Object.assign() | 修改原对象子引用类型,新对象会受影响 |
| 深拷贝 | 层层拷贝,新对象与原对象完全独立 | structuredClone()、JSON.parse(JSON.stringify()) | 各方法有局限(如无法拷贝函数、BigInt等) |
最后小总结
-
基本类型不用纠结深浅拷贝,赋值就是拷贝值;
-
浅拷贝适合简单场景(原对象只有第一层基本类型),高效简洁;
-
深拷贝适合复杂场景(原对象有多层引用类型),保证数据独立,避免意外修改;
-
选择拷贝方法时,根据需求选:
- 需拷贝Date、Map、循环引用 → 用structuredClone();
- 简单对象/数组,无特殊类型 → 用JSON.parse(JSON.stringify())(注意局限);
- 有特殊类型(如函数、Symbol)、需自定义拷贝规则 → 用完善版手动深拷贝;
- 简单数组/对象,无需深层拷贝 → 用扩展运算符、Object.assign()等浅拷贝方法
结合上面的代码实例,能轻松区分浅拷贝和深拷贝,下次遇到拷贝问题,再也不用慌啦!