在前端开发中,“拷贝对象 / 数组” 是高频操作 —— 比如修改表单数据时保留原始值、状态管理中避免副作用、处理接口返回数据时隔离修改。但很多开发者只知 “浅拷贝” 和 “深拷贝” 之名,却踩遍引用传递的坑:修改新对象,原对象莫名被篡改;用 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 方案的核心缺陷:
- 无法拷贝函数、正则、Symbol、BigInt 等特殊类型;
- Date 对象会被转为字符串,失去 Date 类型特性;
- 无法处理循环引用(如 obj.self = obj 会直接报错);
- 忽略原型链上的属性;
- 无法拷贝不可枚举属性。
(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 深浅拷贝的实际应用案例