前言
你有没有过这种抓狂瞬间:明明新建了对象想单独修改,结果原对象跟着 "秒同步" ?就像买了个连轴的 "牵线木偶" ,动新的、老的也跟着晃!这事儿得怪 JS 的引用类型 ------ 对象、数组这类 "特殊选手" 存的不是具体数值,而是指向数据的 "内存地址" 。想让新对象彻底 "自立门户" 不牵连原对象?拷贝,安排上!
一、先搞懂:为啥基础类型不用拷贝?
举个例子:
javascript
let a = 1;
let b = a;
a 和 b 存的是具体数值 ,改 a 不会影响 b------ 这叫 "值传递" 。原理 :基础类型存栈内存,b = a是复制 a 的具体数值到 b 的独立栈空间 ,改 a 只改自身内存,不影响 b。所以你怎么改变a都不会影响b。
但引用类型(比如对象)不一样:
javascript
let obj = {
age: 18
};
let obj2 = obj; // obj2存的是obj的地址!
obj.age = 20;
console.log(obj2.age); // 也变成20了!

这就像你和朋友共用 一个网盘,你改了文件,朋友那边也同步变了 ------ 这就是 "引用传递"。
二、浅拷贝:"表面独立,内核共享"
浅拷贝是 "只拷贝第一层" :新对象的基本类型属性 是独立的,但子对象 / 子数组还是共用地址 ------ 相当于 "表面分了家,钱包还共用"。那么浅拷贝有哪些经典的例子呢?我们一个一个来看看:
1. 数组的浅拷贝:slice(0)
javascript
const arr = ['a', 'b', 'c', 'd', {age: 18}];
const arr2 = arr.slice(0);
arr[4].age = 20;
console.log(arr2);

slice(0)把数组第一层都复制了,但里面的{age:18}是对象,传的是地址 ------ 改原数组里的子对象,新数组也跟着变!
看图更详细:

2. 数组的浅拷贝:扩展运算符[...arr]
javascript
const a = [1, 2, {age: 18}];
let c = [...a];
a[2].age = 19;
console.log(c);

扩展运算符[...arr]看着像是给数组 "开了个全新副本",用起来简洁又酷炫,新手第一眼都会觉得 "这肯定是完全独立了吧?"------ 但真相是:它依旧是浅拷贝 !扩展运算符只把数组第一层的基础类型值 复制了一份,可数组里嵌套的子对象{ age: 18... },拷贝的依旧是内存地址 ------ 就像给你换了个新书包,但书包里的文具盒还是和同桌共用的,他改了文具盒里的笔,你这边也会跟着变。
3. 数组的浅拷贝:concat()
我们先验证他是否创建了一个新对象:
javascript
const a = [1, 2, 3, {age: 18}];
const b = [4, 5];
const c = a.concat(b);
console.log(c, a);

很明显c为创建的新对象。
javascript
const a = [1, 2, 3, { age: 18 }];
const d = a.concat(); // 或者 const d = [].concat(a);
a[3].age = 19;
console.log(d);

concat()做数组浅拷贝时,会复制原数组第一层基础类型元素 到新数组(改原数组第一层基础值不影响新数组),但原数组里的嵌套对象 / 数组 ,只会复制其内存地址到新数组 ------ 改原数组子对象属性,新数组会同步变,子对象仍是 "牵线木偶"。
4. 数组的 "假独立":toReversed()
在看toReversed()之前,我们先看看为什么不用reverse()
javascript
const arr = [1, 2, 3];
const arr2 = arr.reverse();
console.log(arr);

很明显,reverse()并没有创建一个新的对象,而是在原数组上直接修改。所以我们肯定不能用reverse(),于是有请--toReversed()登场!
javascript
const arr = [1, 2, 3];
const arr2 = arr.toReversed().reverse();
console.log(arr2, arr);

toReversed()是 ES2023 的新方法,会返回新数组(浅拷贝),但它仍然是浅拷贝,只复制第一层元素。如果数组里有对象,子对象仍共享地址。
5. 对象的浅拷贝:Object.assign()
我们先介绍一下Object.assign()的用法:
javascript
const obj = {
name: 'henry',
like: ['🏸']
}
const obj2 = {
age: 18
}
const newObj = Object.assign(obj, obj2);
console.log(newObj);

很显然,就是把两个对象里的元素整合到一个新的对象里面了。那么接下来看如何使用Object.assign()来实现浅拷贝:
javascript
const obj = {
name: 'henry',
like: ['🏸']
}
obj.name = 'harvest';
obj.like[0] = '🎤'
const newObj = Object.assign({}, obj);
console.log(newObj);
输出结果:

很明显,Object.assign()把原对象的属性 "搬" 到新对象里,但子对象 / 子数组还是传地址 ------ 所以like数组改了,新对象也变!
6. 手写浅拷贝函数
想真正理解浅拷贝的核心,最好的方式就是自己手搓一个:
javascript
const obj = {
name: 'henry',
age: 18,
like:{
n: '🏸',
m: '🎤'
}
}
function shallowCopy(obj) {
let o = {};
// 遍历原对象,将原对象中的 key, value 都存到新对象中一份
for(let key in obj){
if(obj.hasOwnProperty(key)){ // 只拷贝自身属性
o[key] = obj[key]; // 第一层属性赋值,子对象传地址
}
}
return o;
}
const newObj = shallowCopy(obj);
obj.like.m = '🏀'; // like.m也变成了🏀
console.log(newObj);

这就是浅拷贝的本质:只 "复制第一层",子对象还是共用地址~ 就像你和同事共享一个工作文件夹,你新建了文件夹快捷方式(浅拷贝),快捷方式里的普通文件是独立副本,但嵌套的子文件夹仍指向原地址。
三、深拷贝:"彻底分家,各过各的"
深拷贝是 "层层拷贝":不管对象嵌套多少层,新对象和原对象的所有属性都是独立的------ 相当于 "不仅分家,连钱包都各自买新的"。
但深拷贝也有 "坑",我们一起来看看:
1. 现代浏览器的深拷贝:structuredClone()
javascript
const obj = {
name: 'henry',
age: 18,
like:{
n: '🏸',
m: '🎤'
},
a: 123n, // BigInt
say() {
console.log('hello');
}
}
const newObj = structuredClone(obj);
obj.like.m = '🏀';
console.log(newObj);
// 但注意:structuredClone不支持拷贝函数
console.log(newObj.say); // undefined


structuredClone()是浏览器自带的深拷贝,能处理大部分情况,但不支持函数、bigInt、Symbol等类型。
2. 经典深拷贝:JSON.parse(JSON.stringify(obj))
这是前端常用的 "曲线救国" 深拷贝,但坑特别多:
javascript
const obj = {
name: 'henry',
age: 18,
like:{
n: '🏸',
m: '🎤'
},
say() {
console.log('hello');
},
a: undefined,
b: null,
c: NaN,
d: Infinity
}
const o = JSON.parse(JSON.stringify(obj));
obj.like.m = '🏀';
console.log(o);
// 结果:{name:'henry', like:{n:'🏸',m:'🎤'}, c:null, d:null}
// 消失的:say、a;被篡改的:NaN→null,Infinity→null

JSON.stringify会忽略函数、undefined ,把NaN/Infinity转成null------ 所以这招只适合 "纯 JSON 结构" 的对象!
总结:无法处理
bigint,undefined,NaN,Infinity,function等
3. 手写深拷贝(递归版)
想要自己实现深拷贝,得用递归------ 遇到子对象就继续拷贝,直到所有层都复制完。
先回忆下递归的逻辑,比如计算阶乘:
javascript
function mul(n){
if(n == 1){
return 1;
}
return n * mul(n-1);
}
console.log(mul(5));
这样我们就很轻松的得出了5的阶层:

深拷贝的递归思路就是:如果当前属性是对象,就递归拷贝;否则直接赋值。
简易手写版:
javascript
const obj = {
name: 'henry',
age: 18,
like: {
n: '🏸',
m: '🎤'
}
}
function deepClone(obj) {
let o = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
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); // 还是🎤(彻底独立!)

这样就实现了 "层层拷贝",新对象和原对象完全独立啦~
四、浅拷贝 VS 深拷贝 怎么选?按场景选对不踩坑~
| 拷贝类型 | 特点 | 适用场景 |
|---|---|---|
| 浅拷贝 | 只拷贝第一层,子对象共享地址 | 只需要第一层独立的简单对象 / 数组 |
| 深拷贝 | 层层拷贝,完全独立 | 嵌套层级多、需要彻底分离的对象 |
选拷贝方式就像给数据选 "独立程度":
- 浅拷贝:适合数据只有一层结构 (比如
['a', 'b', 'c']、{name: '张三', age: 20}),或不需要修改子对象 / 子数组的场景。比如快速复制列表展示、临时复用基础属性,浅拷贝效率高,代码也简洁([...arr]、Object.assign随手就用)。 - 深拷贝:适合数据嵌套多层 (比如
{user: {info: {age: 18}}}),且需要修改深层属性又不想影响原数据的场景。比如表单提交前的草稿复制、复杂数据的缓存备份 ------ 但要注意:简单场景用structuredClone,需兼容特殊值(函数、BigInt)就手写递归深拷贝,别盲目用JSON.parse(JSON.stringify)踩坑。
结语
其实拷贝没有 "谁更好",只有 "谁更合适" 。浅拷贝是 "够用就好" 的高效选择,深拷贝是 "彻底隔离" 的稳妥方案。搞懂引用类型的本质,再结合数据结构和业务场景选拷贝方式,就能告别 "改新对象、原对象乱跳" 的崩溃时刻,让 JS 对象乖乖听你指挥~
最后送你个小口诀:
- 浅拷贝:"表面兄弟,内核共享"
- 深拷贝:"彻底分家,各自安好"