news 2026/4/15 15:02:49

吃透 JS 深浅拷贝:从原理到实战,避坑指南全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
吃透 JS 深浅拷贝:从原理到实战,避坑指南全解析

在前端开发中,“拷贝对象 / 数组” 是高频操作 —— 比如修改表单数据时保留原始值、状态管理中避免副作用、处理接口返回数据时隔离修改。但很多开发者只知 “浅拷贝” 和 “深拷贝” 之名,却踩遍引用传递的坑:修改新对象,原对象莫名被篡改;用 JSON.parse (JSON.stringify ()) 拷贝,函数、正则直接丢失…

本文将从 JS 数据存储的底层逻辑出发,拆解深浅拷贝的本质,手把手实现可靠的深浅拷贝函数,梳理不同场景下的最优方案,帮你彻底避开拷贝的那些 “坑”。

一、先搞懂:为什么会有深浅拷贝?

深浅拷贝的核心差异,源于 JS 对基本类型引用类型的不同存储机制 —— 这是理解拷贝的 “根”,绕开这个讲拷贝都是空谈。

1. 数据类型的存储规则

类型分类包含类型存储位置访问方式
基本类型String/Number/Boolean/Null/Undefined/Symbol/BigInt栈内存(Stack)直接访问值,赋值时拷贝 “值本身”
引用类型Object/Array/Function/RegExp/Date 等堆内存(Heap)栈中存 “引用地址”,指向堆中数据;赋值时拷贝 “地址” 而非 “数据”

2. 举个例子:赋值≠拷贝

先看一段简单代码,理解 “引用传递” 的坑:

javascript

运行

// 基本类型:赋值即拷贝值 let a = 10; let b = a; b = 20; console.log(a); // 10(b修改不影响a) // 引用类型:赋值仅拷贝地址 let obj1 = { name: "张三", age: 20 }; let obj2 = obj1; obj2.name = "李四"; console.log(obj1.name); // 李四(obj2和obj1指向同一个堆内存地址)

正是因为引用类型的 “地址拷贝” 特性,才需要专门的 “浅拷贝” 和 “深拷贝” 来实现真正的 “数据隔离”。

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

1. 浅拷贝的定义

浅拷贝会创建一个新对象 / 数组,但仅拷贝第一层属性

  • 若第一层属性是基本类型,拷贝 “值本身”;
  • 若第一层属性是引用类型,拷贝 “引用地址”(新、旧对象共享深层数据)。

2. 浅拷贝的实现方式

(1)原生 API 实现(简单场景首选)
① Object.assign()

javascript

运行

const obj1 = { name: "张三", hobby: ["篮球", "游戏"] }; const obj2 = Object.assign({}, obj1); // 基本类型属性:修改不影响原对象 obj2.name = "李四"; console.log(obj1.name); // 张三 // 引用类型属性:修改会影响原对象 obj2.hobby.push("读书"); console.log(obj1.hobby); // ["篮球", "游戏", "读书"]

⚠️ 注意:Object.assign () 会忽略原型链上的属性,且只拷贝可枚举属性。

② 展开运算符(...)

语法更简洁,效果与 Object.assign () 一致:

javascript

运行

const obj1 = { name: "张三", hobby: ["篮球", "游戏"] }; const obj2 = { ...obj1 }; obj2.hobby.push("跑步"); console.log(obj1.hobby); // ["篮球", "游戏", "跑步"] // 数组浅拷贝同理 const arr1 = [1, 2, [3, 4]]; const arr2 = [...arr1]; arr2[2].push(5); console.log(arr1[2]); // [3, 4, 5]
③ 数组专用:slice ()/concat ()

javascript

运行

const arr1 = [1, 2, [3, 4]]; const arr2 = arr1.slice(); // 无参数时拷贝整个数组 const arr3 = arr1.concat(); arr2[2].push(5); console.log(arr1[2]); // [3, 4, 5] console.log(arr3[2]); // [3, 4, 5]
(2)手动实现浅拷贝函数(理解原理)

javascript

运行

function shallowClone(target) { // 排除非引用类型(基本类型直接返回值) if (typeof target !== "object" || target === null) { return target; } // 判断是数组还是对象,创建新容器 const cloneTarget = Array.isArray(target) ? [] : {}; // 遍历第一层属性,赋值(仅拷贝地址/基本值) for (let key in target) { // 只拷贝自身属性(排除原型链属性) if (target.hasOwnProperty(key)) { cloneTarget[key] = target[key]; } } return cloneTarget; } // 测试 const obj = { a: 1, b: [2, 3] }; const cloneObj = shallowClone(obj); cloneObj.b.push(4); console.log(obj.b); // [2, 3, 4](验证浅拷贝特性)

3. 浅拷贝的适用场景

  • 仅需拷贝 “单层结构” 的对象 / 数组(如 { name: "张三", age: 20 });
  • 性能优先,无需隔离深层数据(如临时修改表层属性);
  • 数组 / 对象的 “快速复制”(如避免直接赋值导致的引用关联)。

三、深拷贝:彻底隔离数据的 “终极方案”

1. 深拷贝的定义

深拷贝会创建一个全新的对象 / 数组,递归拷贝所有层级的属性:无论是基本类型还是引用类型,新对象与原对象完全隔离,修改新对象不会影响原对象。

2. 深拷贝的实现方式

(1)简易方案:JSON.parse (JSON.stringify ())

这是最常用的 “快捷方案”,但有明显局限性:

javascript

运行

const obj1 = { name: "张三", age: 20, hobby: ["篮球", "游戏"], birthday: new Date("2000-01-01"), sayHi: () => console.log("hi"), reg: /^1[3-9]\d{9}$/, symbol: Symbol("test") }; const obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj2); // 输出结果: // { // name: "张三", // age: 20, // hobby: ["篮球", "游戏"], // birthday: "2000-01-01T00:00:00.000Z", // Date被转为字符串 // // sayHi、reg、symbol 直接丢失! // }

⚠️ JSON 方案的核心缺陷:

  1. 无法拷贝函数、正则、Symbol、BigInt 等特殊类型;
  2. Date 对象会被转为字符串,失去 Date 类型特性;
  3. 无法处理循环引用(如 obj.self = obj 会直接报错);
  4. 忽略原型链上的属性;
  5. 无法拷贝不可枚举属性。
(2)手动实现深拷贝函数(进阶:处理边界场景)

一个 “合格” 的深拷贝函数需要解决:递归拷贝、特殊类型处理、循环引用问题。

javascript

运行

function deepClone(target, map = new WeakMap()) { // 1. 基本类型/函数:直接返回(函数无需深拷贝,拷贝引用即可) if (typeof target !== "object" || target === null) { return target; } // 2. 处理循环引用(避免无限递归) if (map.has(target)) { return map.get(target); } // 3. 处理特殊引用类型 let cloneTarget; // 3.1 处理Date if (target instanceof Date) { cloneTarget = new Date(target); map.set(target, cloneTarget); return cloneTarget; } // 3.2 处理RegExp if (target instanceof RegExp) { cloneTarget = new RegExp(target.source, target.flags); map.set(target, cloneTarget); return cloneTarget; } // 3.3 处理Array/Object cloneTarget = Array.isArray(target) ? [] : {}; // 缓存当前对象,解决循环引用 map.set(target, cloneTarget); // 4. 递归拷贝所有层级属性 for (let key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone(target[key], map); } } // 5. 处理Symbol属性(ES6+) const symbolKeys = Object.getOwnPropertySymbols(target); for (let symbolKey of symbolKeys) { if (target.hasOwnProperty(symbolKey)) { cloneTarget[symbolKey] = deepClone(target[symbolKey], map); } } return cloneTarget; } // 测试:覆盖边界场景 const obj = { a: 1, b: [2, 3], c: new Date(), d: /abc/g, e: Symbol("test"), f: () => console.log("test") }; // 模拟循环引用 obj.self = obj; const cloneObj = deepClone(obj); cloneObj.b.push(4); console.log(obj.b); // [2, 3](深层数据隔离) console.log(cloneObj.c instanceof Date); // true(Date类型保留) console.log(cloneObj.d instanceof RegExp); // true(RegExp类型保留) console.log(cloneObj.self === cloneObj); // true(循环引用处理正常)
(3)成熟方案:Lodash.cloneDeep ()

生产环境推荐使用 Lodash 的cloneDeep方法 —— 经过海量场景验证,处理了所有边界情况:

javascript

运行

// 安装:npm i lodash const _ = require("lodash"); const obj = { name: "张三", hobby: ["篮球", "游戏"], reg: /^1[3-9]\d{9}$/, self: obj // 循环引用 }; const cloneObj = _.cloneDeep(obj); cloneObj.hobby.push("读书"); console.log(obj.hobby); // ["篮球", "游戏"](完全隔离)

3. 深拷贝的适用场景

  • 需要完全隔离原数据和新数据(如表单提交前保留原始值、状态管理中的不可变数据);
  • 处理多层嵌套的复杂对象(如接口返回的嵌套数据 {user: { info: { age: 20} } });
  • 避免引用传递导致的 “意外修改”(如全局配置对象的拷贝)。

四、深浅拷贝核心对比

维度浅拷贝深拷贝
拷贝层级仅第一层所有层级(递归拷贝)
数据隔离表层隔离,深层共享引用完全隔离,无任何引用关联
性能高效(仅遍历第一层)较低(递归遍历所有层级)
特殊类型处理仅拷贝引用,不处理特殊类型可处理 Date/RegExp/Symbol 等
循环引用无处理(不会报错但共享引用)需手动处理(否则无限递归)
适用场景单层结构、性能优先多层结构、数据隔离优先

五、避坑指南 & 最佳实践

1. 常见误区

  • ❌ 认为 “展开运算符 / Object.assign () 是深拷贝”:仅拷贝第一层,深层仍共享引用;
  • ❌ 滥用 JSON.parse (JSON.stringify ()):忽略特殊类型丢失的问题;
  • ❌ 深拷贝函数未处理循环引用:导致栈溢出(Maximum call stack size exceeded);
  • ❌ 深拷贝函数未处理 Symbol 属性:ES6 + 场景下数据丢失。

2. 性能优化建议

  • 非必要不深拷贝:浅拷贝能满足的场景,优先用浅拷贝(如单层对象);
  • 大数据量深拷贝:优先选择 Lodash.cloneDeep(手写递归性能较差);
  • 避免频繁深拷贝:可通过 “不可变数据模式”(如 Immer 库)减少拷贝次数。

3. 生产环境选型

场景推荐方案
单层对象 / 数组展开运算符(...)/Object.assign ()
复杂嵌套对象(无特殊类型)JSON.parse(JSON.stringify())
复杂嵌套对象(含特殊类型)Lodash.cloneDeep()
需自定义拷贝规则手写深拷贝函数(扩展逻辑)

六、总结

JS 深浅拷贝的本质,是对 “基本类型值传递” 和 “引用类型地址传递” 的补充处理:

  • 浅拷贝是 “表面功夫”,解决第一层数据的隔离,性能高但不彻底;
  • 深拷贝是 “釜底抽薪”,递归拷贝所有层级,彻底隔离数据但性能成本高。

开发中无需盲目追求 “深拷贝”,核心是根据场景选择:

  • 简单场景用浅拷贝,兼顾性能;
  • 复杂场景用成熟的深拷贝方案(如 Lodash),避免手写函数的边界漏洞;
  • 始终牢记:拷贝的核心目标是 “按需隔离数据”,而非 “为了拷贝而拷贝”。

吃透深浅拷贝,不仅能避开 90% 的 “数据篡改” bug,更能理解 JS 数据存储的底层逻辑 —— 这也是从 “初级前端” 到 “中级前端” 的关键一步。

编辑分享

写一篇 1000 字的关于 JS 深浅拷贝的博客文

如何在博客中展示 JS 深浅拷贝的性能分析数据?

分享一些 JS 深浅拷贝的实际应用案例

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

qView:为什么这个极简图片查看器能让你告别卡顿烦恼?

qView:为什么这个极简图片查看器能让你告别卡顿烦恼? 【免费下载链接】qView Practical and minimal image viewer 项目地址: https://gitcode.com/gh_mirrors/qv/qView 你是否曾经因为图片查看器启动缓慢而错失重要时刻?当其他软件还…

作者头像 李华
网站建设 2026/4/12 11:39:54

通义千问AI大模型本地部署实战:从零开始的智能助手搭建

通义千问AI大模型本地部署实战:从零开始的智能助手搭建 【免费下载链接】通义千问 FlashAI一键本地部署通义千问大模型整合包 项目地址: https://ai.gitcode.com/FlashAI/qwen 想要在个人电脑上拥有一个专属的AI助手吗?通义千问大模型结合FlashAI…

作者头像 李华
网站建设 2026/4/14 23:46:16

Visual Studio中的静态成员和非静态成员

一、核心区别对比特性静态成员非静态成员归属主题类(Class)本身类的实例对象内存分配时机类第一次被访问时(程序启动后)类实例化(new)时内存位置全局数据区(静态存储区)堆内存&#…

作者头像 李华
网站建设 2026/4/8 13:58:32

计算机毕业设计springboot基于spring+协同过滤推荐算法的电影周边商城系统 基于Spring Boot的电影周边电商平台设计与实现 Spring Boot框架下电影周边商城信息管理系统开发

计算机毕业设计springboot基于spring协同过滤推荐算法的电影周边商城系统177o59 (配套有源码 程序 mysql数据库 论文) 本套源码可以在文本联xi,先看具体系统功能演示视频领取,可分享源码参考。随着互联网技术的飞速发展,电影周边市…

作者头像 李华
网站建设 2026/4/8 13:44:27

哔哩下载姬DownKyi终极指南:简单高效获取B站优质内容

哔哩下载姬DownKyi是一款专业的B站视频下载工具,能够帮助用户快速保存和管理喜欢的视频内容。这款免费工具支持批量下载、8K超高清画质,并提供丰富的音视频处理功能,让你的内容管理变得轻松简单。 【免费下载链接】downkyi 哔哩下载姬downkyi…

作者头像 李华