从 “牵线木偶” 到 “独立个体”:JS 拷贝的爱恨情仇(浅拷贝 VS 深拷贝)

前言

你有没有过这种抓狂瞬间:明明新建了对象想单独修改,结果原对象跟着 "秒同步" ?就像买了个连轴的 "牵线木偶" ,动新的、老的也跟着晃!这事儿得怪 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 对象乖乖听你指挥~

最后送你个小口诀:

  • 浅拷贝:"表面兄弟,内核共享"
  • 深拷贝:"彻底分家,各自安好"
相关推荐
漫天黄叶远飞41 分钟前
地址与地基:在 JavaScript 的堆栈迷宫里,重新理解“复制”的哲学
前端·javascript·面试
杨啸_新房客1 小时前
如何优雅的设置公司的NPM源
前端·npm
ohyeah1 小时前
深入理解 JavaScript 中的继承与 instanceof 原理
前端·javascript
linhuai1 小时前
flutter如何实现有登陆权限管理
前端
crary,记忆1 小时前
React 之 useEffect
前端·javascript·学习·react.js
BD_Marathon1 小时前
【JavaWeb】HTML常见标签——标题段落和换行
前端·html
小飞侠在吗1 小时前
vue OptionsAPI与CompositionAPI
前端·javascript·vue.js
春卷同学1 小时前
基于Electron开发的跨平台鸿蒙PC找不同游戏应用
javascript·游戏·electron
天涯路s1 小时前
qt怎么将模块注册成插件
java·服务器·前端·qt