news 2026/4/26 1:02:26

彻底搞懂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/4/18 6:09:42

Java版LeetCode热题100之单词拆分:从动态规划到面试实战的全面解析

Java版LeetCode热题100之单词拆分:从动态规划到面试实战的全面解析 本文深入剖析 LeetCode 第139题「单词拆分」,涵盖题目理解、算法设计、代码实现、复杂度分析、优化思路、数据结构基础、面试应对策略以及实际应用场景等多个维度,是一篇面向…

作者头像 李华
网站建设 2026/4/24 13:47:15

2026必备!8个AI论文平台,助你轻松搞定本科生毕业论文!

2026必备!8个AI论文平台,助你轻松搞定本科生毕业论文! AI 工具正在重塑论文写作的未来 在 2026 年,随着人工智能技术的不断进步,越来越多的学生开始依赖 AI 工具来辅助完成毕业论文。从最初的资料搜集到最终的格式调整…

作者头像 李华
网站建设 2026/4/25 5:01:15

构建自主同城配送平台,开源跑腿小程序系统的核心优势

温馨提示:文末有资源获取方式对于希望摆脱平台高额抽成、建立独立品牌与客户关系的创业者而言,一套功能完备、自主可控的技术方案至关重要。源码获取方式在源码闪购网。完全独立的品牌运营与用户沉淀系统支持私有化部署与全套UI自定义(包括小…

作者头像 李华
网站建设 2026/4/21 10:11:47

java_ssm11办公电子政务管理系统 上下班考勤打卡系统

目录 具体实现截图办公电子政务管理系统与上下班考勤打卡系统摘要 系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 :文章底部获取博主联系方式! 具体实现截图 办公电子政务管理系统与上下班考勤打卡系统摘要 办公电子政务管理系统是基于…

作者头像 李华