news 2026/3/8 7:27:31

彻底搞懂JavaScript深拷贝与浅拷贝:从原理到实战避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
彻底搞懂JavaScript深拷贝与浅拷贝:从原理到实战避坑

在JavaScript日常开发中,数据拷贝是绕不开的操作,但很多人都踩过“改新对象、原对象跟着变”的坑,排查半天发现根源是没分清浅拷贝和深拷贝。这篇文章从底层存储机制讲起,结合实际业务场景和可直接复用的代码案例,把深浅拷贝的核心逻辑、实现方式和边界情况讲透,帮大家在开发中精准选型、避免踩雷。

一、前置核心:JS数据类型的存储逻辑

想分清深浅拷贝,必须先吃透JS数据类型的存储规则——这是所有拷贝行为的底层逻辑,没搞懂这个,后面的内容都是空中楼阁。

1. 数据类型分类及存储差异

JS数据分两大类,存储位置和拷贝特性完全不同,直接决定了拷贝后的表现:

类型分类

包含具体类型

存储位置

核心拷贝特点

基本数据类型

Number、String、Boolean、Null、Undefined、Symbol、BigInt

栈内存(Stack)

拷贝时直接复制「值」,新老数据相互独立,修改互不影响

引用数据类型

Object(数组、普通对象、函数、Date、RegExp等)

栈存引用地址,堆存实际值(Heap)

默认拷贝「引用地址」,新老对象共享同一块堆内存数据

2. 直观对比:基本类型与引用类型拷贝差异

基本类型拷贝:本质是值拷贝,天然具备“深拷贝”的效果,修改新变量不会牵连原变量。

// 基本类型拷贝示例 let num1 = 100; let num2 = num1; // 直接复制num1的值 num2 = 200; // 修改新变量 console.log(num1); // 100(原数据不受影响) console.log(num2); // 200(新数据独立)

引用类型默认拷贝:只复制引用地址,新老对象指向同一块堆内存,改一个就会影响另一个,这就是浅拷贝的核心问题。

// 引用类型默认浅拷贝 let obj1 = { name: "张三", age: 22 }; let obj2 = obj1; // 复制的是引用地址,而非实际数据 obj2.name = "李四"; // 修改新对象属性 console.log(obj1.name); // 李四(原对象被同步修改) console.log(obj2.name); // 李四

关键结论:浅拷贝和深拷贝的差异,仅存在于引用数据类型中。基本类型的拷贝都是“值拷贝”,不存在深浅之分。

二、浅拷贝:只做一层的“表面功夫”

1. 浅拷贝的核心特性

浅拷贝针对引用数据类型,仅复制对象表层结构——也就是栈内存引用地址对应的第一层数据。如果对象存在多层嵌套(比如对象里套对象、数组里套对象),内层的引用类型依然共享堆内存,修改内层数据会同步影响原对象。

2. 常用浅拷贝实现方式(附实战案例)

(1)数组浅拷贝

数组的slice()、concat()方法,以及ES6扩展运算符,都是常用的浅拷贝方式,用法简单但要注意内层数据共享的问题。

const arr1 = [1, 2, { score: 90 }]; // 三种常用浅拷贝方式 const arr2 = [...arr1]; // 扩展运算符(最简洁) const arr3 = arr1.slice(); // slice方法,不传参复制全量 const arr4 = arr1.concat(); // concat方法,空参数拼接 // 修改内层嵌套对象 arr2[2].score = 100; // 所有浅拷贝数组的内层数据都会同步变化 console.log(arr1[2].score); // 100(原数组受影响) console.log(arr3[2].score); // 100 console.log(arr4[2].score); // 100
(2)对象浅拷贝

Object.assign()和ES6扩展运算符,是对象浅拷贝的主流方式,适合单层对象的复用场景。

const obj1 = { a: 1, b: { c: 2 } }; // 两种对象浅拷贝方式 const obj2 = Object.assign({}, obj1); // Object.assign const obj3 = { ...obj1 }; // 扩展运算符(更直观) // 修改内层嵌套对象 obj2.b.c = 3; // 原对象和其他浅拷贝对象的内层数据同步变化 console.log(obj1.b.c); // 3(原对象受影响) console.log(obj3.b.c); // 3

3. 浅拷贝的适用场景

浅拷贝性能开销小、实现简单,不用做递归处理,适合以下场景:

  • 对象/数组为单层结构,无任何嵌套的引用类型;

  • 仅复用表层数据,且内层数据为只读状态,不涉及修改操作。

三、深拷贝:完全独立的“全量复制”

1. 深拷贝的核心特性

深拷贝会递归遍历引用数据类型的所有层级,把每一层的实际数据都复制到新的堆内存中,最终生成一个和原对象完全独立的新对象。无论修改新对象的哪一层数据,都不会对原对象产生任何影响。

2. 三种深拷贝实现方式(从简单到生产可用)

(1)简易版:JSON.parse(JSON.stringify())

这是日常开发中最常用的快速深拷贝方案,不用依赖任何库,一行代码就能实现,适合简单场景。

const obj1 = { name: "张三", info: { age: 22, hobby: ["篮球", "游戏"] } }; // 一行代码实现深拷贝 const obj2 = JSON.parse(JSON.stringify(obj1)); // 修改新对象的内层数据 obj2.info.age = 25; obj2.info.hobby[0] = "足球"; // 原对象数据不受影响 console.log(obj1.info.age); // 22 console.log(obj1.info.hobby[0]); // 篮球

注意局限性:这种方式有明显短板,生产环境需谨慎使用,不支持以下场景:

  • 无法拷贝函数、Undefined、Symbol(会被直接忽略);

  • 无法处理循环引用对象(会直接报错,导致程序中断);

  • 特殊对象拷贝失真(Date转为字符串、RegExp转为空对象)。

我们用一个案例直观感受这些问题:

const obj = { fn: () => console.log('test'), // 函数类型 time: new Date(), // Date类型 undef: undefined, // Undefined类型 sym: Symbol('foo') // Symbol类型 }; const copyObj = JSON.parse(JSON.stringify(obj)); console.log(copyObj.fn); // undefined(函数被忽略) console.log(copyObj.time); // 字符串(Date失真) console.log(copyObj.undef); // 不存在(Undefined被忽略) console.log(copyObj.sym); // 不存在(Symbol被忽略)
(2)进阶版:手动实现递归深拷贝(兼容特殊场景)

手动实现深拷贝能帮我们吃透底层逻辑,还能自定义处理特殊类型。下面是优化后的完整实现,兼容Date、RegExp和循环引用,可直接用于中小型项目。

/** * 优化版递归深拷贝(兼容Date、RegExp、循环引用) * @param {*} target 要拷贝的目标数据 * @param {WeakMap} map 缓存已拷贝对象,解决循环引用 * @returns 拷贝后的新数据 */ function deepClone(target, map = new WeakMap()) { // 1. 处理基本类型和null(直接返回值,无需拷贝) if (typeof target !== 'object' || target === null) { return target; } // 2. 处理循环引用:已拷贝过则直接返回缓存对象,避免死循环 if (map.has(target)) { return map.get(target); } // 3. 处理Date类型:创建新Date对象,保留原时间 if (target instanceof Date) { const newDate = new Date(target.getTime()); map.set(target, newDate); return newDate; } // 4. 处理RegExp类型:保留原正则表达式的源和修饰符 if (target instanceof RegExp) { const newReg = new RegExp(target.source, target.flags); map.set(target, newReg); return newReg; } // 5. 处理数组:创建新数组,递归拷贝每一项 if (target instanceof Array) { const newArr = []; map.set(target, newArr); target.forEach(item => newArr.push(deepClone(item, map))); return newArr; } // 6. 处理普通对象:创建新对象,遍历自身属性递归拷贝 if (target instanceof Object) { const newObj = {}; map.set(target, newObj); // 只拷贝自身可枚举属性,避免拷贝原型链上的属性 for (const key in target) { if (target.hasOwnProperty(key)) { newObj[key] = deepClone(target[key], map); } } return newObj; } } // 测试:包含循环引用和特殊类型 const obj1 = { a: 1, date: new Date(), reg: /test/g, arr: [1, { b: 2 }] }; // 制造循环引用(obj1指向自身) obj1.self = obj1; const obj2 = deepClone(obj1); // 验证拷贝效果 console.log(obj2.date instanceof Date); // true(Date类型正常) console.log(obj2.reg instanceof RegExp); // true(RegExp类型正常) console.log(obj2.self === obj2); // true(循环引用处理正常) // 修改新对象内层数据,验证独立性 obj2.arr[1].b = 100; console.log(obj1.arr[1].b); // 2(原数据无变化)

这个实现的核心优化点:用WeakMap缓存已拷贝对象,解决循环引用导致的递归死循环;针对性处理Date和RegExp类型,避免拷贝失真,基本能覆盖大部分业务场景。

(3)生产版:第三方库lodash.cloneDeep()

生产环境中,不建议自己造轮子——成熟的第三方库已经处理了所有边界情况(包括函数、循环引用、特殊对象等),稳定性和兼容性更有保障。最常用的就是lodash的cloneDeep()方法。

// 1. 安装lodash(npm/yarn) // npm install lodash --save // yarn add lodash // 2. 引入并使用(可按需引入,减少体积) const _ = require('lodash'); // 按需引入方式:const cloneDeep = require('lodash/cloneDeep'); const obj1 = { name: "李四", fn: () => console.log("hello deep clone"), // 函数类型 date: new Date(), // Date类型 reg: /test/g, // RegExp类型 info: { age: 24 } }; const obj2 = _.cloneDeep(obj1); // 修改新对象内层数据 obj2.info.age = 26; // 验证效果:原对象无变化,特殊类型拷贝正常 console.log(obj1.info.age); // 24 console.log(obj2.fn()); // hello deep clone(函数正常) console.log(obj2.date instanceof Date); // true(Date正常)

四、实战选型指南:深浅拷贝怎么选?

开发中不用盲目追求深拷贝,结合业务场景选型才是最优解,既保证功能又兼顾性能。整理了常见场景的选型建议:

业务场景

推荐拷贝方式

选择理由

单层对象/数组,无嵌套引用类型

浅拷贝(扩展运算符/Object.assign)

性能最优,实现简单,无多余递归开销

多层嵌套引用类型,需完全独立数据

深拷贝

避免修改新数据影响原对象,保证数据安全性

数据含函数、循环引用、特殊对象

lodash.cloneDeep()

JSON方式无法处理,手动实现成本高,库更稳定

简单演示、临时数据处理(无特殊类型)

JSON.parse(JSON.stringify())

一行代码搞定,无需依赖第三方库,高效便捷

五、总结与实战技巧

其实深浅拷贝的核心很简单:浅拷贝抄“地址”,深拷贝抄“全量数据”。记住以下几点,就能在开发中灵活应对:

  1. 基本类型拷贝都是值拷贝,深浅拷贝仅针对引用类型;

  2. 浅拷贝适合单层结构,深拷贝适合多层嵌套结构,避免过度使用深拷贝(性能开销更大);

  3. 生产环境优先用lodash.cloneDeep(),简易场景用JSON方法,学习/面试场景可手动实现递归拷贝;

  4. 快速验证是否为深拷贝:用“===”判断新老对象,若结果为false,且内层属性修改互不影响,即为深拷贝。

深浅拷贝是JS基础中的重点,也是面试高频考点,吃透底层原理和选型逻辑,不仅能避免开发踩坑,还能应对面试中的延伸问题。文中所有代码都已实测可用,大家可以直接复制到项目中复用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/4 11:58:21

基于SpringBoot+Vue技术的医院运营管理系统的设计与实现

医院运营管理系统的背景 医疗行业信息化建设已成为现代医院管理的核心需求。传统医院管理模式依赖纸质记录和人工操作,存在效率低、数据孤岛、决策滞后等问题。随着医疗资源紧张和患者需求多样化,亟需通过数字化手段优化运营流程。国家卫健委《医院智慧…

作者头像 李华
网站建设 2026/3/4 9:57:37

支持所有自定义数据时间段查询与导出的跑腿小程序源码系统

温馨提示:文末有资源获取方式在竞争激烈的同城配送领域,精细化运营是制胜关键。本文将深入解析一款专为数据化运营设计的开源同城跑腿小程序源码系统。该系统不仅提供标准化的跑腿业务框架,更通过一系列智能管理功能,赋能运营者实…

作者头像 李华
网站建设 2026/3/4 12:32:25

【开题答辩全过程】以 基于Springboot 的水果售卖系统的设计与开发为例,包含答辩的问题和答案

个人简介 一名14年经验的资深毕设内行人,语言擅长Java、php、微信小程序、Python、Golang、安卓Android等 开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。 感谢大家…

作者头像 李华
网站建设 2026/3/7 5:30:22

java_ssm3旅行日记旅行攻略分享系统

目录 具体实现截图基于SSM框架的旅行日记与攻略分享系统设计与实现 系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 :文章底部获取博主联系方式! 具体实现截图 基于SSM框架的旅行日记与攻略分享系统设计与实现 该系统采用Java语言开发&a…

作者头像 李华