深拷贝和浅拷贝是 JavaScript 中处理引用类型数据(对象、数组等)的核心概念,二者的本质区别在于是否复制引用类型的深层嵌套数据,直接影响数据操作的独立性,是开发中避免数据污染的关键。
一、先明确:为什么需要拷贝?(引用类型的特性)
JavaScript 数据类型分为两类,拷贝行为仅对引用类型有区分(原始类型为值传递,不存在深浅拷贝):
| 数据类型类别 | 包含类型 | 拷贝特性 |
|---|---|---|
| 原始类型 | String、Number、Boolean、Null、Undefined、Symbol、BigInt | 赋值 / 拷贝时传递「值本身」,修改新值不会影响原值 |
| 引用类型 | Object(普通对象、数组、函数、正则等) | 赋值 / 浅拷贝时传递「内存地址(引用)」,修改新数据会影响原数据;深拷贝才会复制数据本身,实现完全独立 |
示例:引用类型的默认赋值(引用传递,非拷贝)
js
// 引用类型:数组
const arr1 = [1, 2, { name: "张三" }];
const arr2 = arr1; // 仅传递引用,不是拷贝
arr2[0] = 100;
arr2[2].name = "李四";
console.log(arr1); // [100, 2, { name: "李四" }](原值被修改)
console.log(arr2); // [100, 2, { name: "李四" }]
二、浅拷贝(Shallow Copy):仅复制表层数据
1. 核心定义
浅拷贝是指只复制引用类型的表层属性(第一层数据) ,对于深层嵌套的引用类型(如对象中的对象、数组中的数组),仅复制其内存地址(引用),新旧数据的深层嵌套部分会共享同一块内存,修改其中一个的深层数据会影响另一个。
2. 常见实现方式
(1)数组浅拷贝
-
Array.prototype.slice()jsconst arr1 = [1, 2, { age: 25 }]; const arr2 = arr1.slice(); // 浅拷贝数组 // 修改表层数据:不影响原值 arr2[0] = 100; console.log(arr1[0]); // 1 console.log(arr2[0]); // 100 // 修改深层引用类型:影响原值 arr2[2].age = 30; console.log(arr1[2].age); // 30(原值被修改) console.log(arr2[2].age); // 30 -
Array.prototype.concat()jsconst arr1 = [1, 2, { age: 25 }]; const arr2 = arr1.concat(); // 浅拷贝 -
扩展运算符
[...arr]jsconst arr1 = [1, 2, { age: 25 }]; const arr2 = [...arr1]; // 浅拷贝
(2)对象浅拷贝
-
Object.assign(target, ...sources)jsconst obj1 = { name: "张三", info: { age: 25 } }; const obj2 = Object.assign({}, obj1); // 浅拷贝到空对象 // 修改表层数据:不影响原值 obj2.name = "李四"; console.log(obj1.name); // 张三 console.log(obj2.name); // 李四 // 修改深层引用类型:影响原值 obj2.info.age = 30; console.log(obj1.info.age); // 30(原值被修改) console.log(obj2.info.age); // 30 -
扩展运算符
{...obj}jsconst obj1 = { name: "张三", info: { age: 25 } }; const obj2 = { ...obj1 }; // 浅拷贝
3. 浅拷贝的特点
- 优点:实现简单、性能开销小,适合仅包含表层数据的引用类型;
- 缺点:无法独立深层嵌套数据,修改深层数据会造成原数据污染;
- 适用场景:只需复制表层数据,无需修改深层嵌套内容的场景(如展示数据副本、临时修改表层属性)。
三、深拷贝(Deep Copy):复制所有层级数据
1. 核心定义
深拷贝是指递归复制引用类型的所有层级数据,不仅复制表层属性,还会对深层嵌套的每个引用类型都创建独立的副本,新旧数据完全隔离,修改其中一个不会影响另一个,实现真正意义上的 "复制"。
2. 常见实现方式
(1)JSON 序列化 / 反序列化(简单场景首选)
通过 JSON.stringify() 将对象转为 JSON 字符串,再通过 JSON.parse() 解析为新对象,实现深拷贝。
js
const obj1 = { name: "张三", info: { age: 25 }, hobbies: ["篮球", "游戏"] };
const obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝
// 修改表层数据:不影响原值
obj2.name = "李四";
// 修改深层数据:不影响原值
obj2.info.age = 30;
obj2.hobbies[0] = "足球";
console.log(obj1.name); // 张三
console.log(obj1.info.age); // 25
console.log(obj1.hobbies[0]); // 篮球
console.log(obj2.name); // 李四
console.log(obj2.info.age); // 30
console.log(obj2.hobbies[0]); // 足球
注意:JSON 方式的局限性(无法处理特殊类型)
- 无法拷贝函数、正则表达式、Date 对象(会转为字符串 / 对象字面量,丢失原有特性);
- 无法拷贝 Symbol 类型属性、undefined 类型属性(会被忽略);
- 无法处理循环引用(如
obj.a = obj,会报错)。
(2)手动递归实现(灵活可控,支持特殊类型)
通过递归遍历对象 / 数组的每一层,对原始类型直接赋值,对引用类型创建新副本,可自定义处理特殊类型。
js
// 深拷贝工具函数
function deepClone(target) {
// 1. 处理原始类型和 null
if (typeof target !== "object" || target === null) {
return target;
}
// 2. 处理 Date 对象
if (target instanceof Date) {
return new Date(target);
}
// 3. 处理 RegExp 对象
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// 4. 处理数组和普通对象(创建新副本)
const result = Array.isArray(target) ? [] : {};
// 5. 递归遍历,拷贝所有层级属性
for (let key in target) {
// 仅拷贝自身属性,不拷贝原型链属性
if (target.hasOwnProperty(key)) {
result[key] = deepClone(target[key]);
}
}
return result;
}
// 测试
const obj1 = {
name: "张三",
info: { age: 25 },
hobbies: ["篮球", "游戏"],
birth: new Date("1999-01-01"),
reg: /abc/gi,
fn: () => console.log("hello")
};
const obj2 = deepClone(obj1);
obj2.info.age = 30;
obj2.birth.setFullYear(2000);
obj2.fn = () => console.log("world");
console.log(obj1.info.age); // 25(不影响原值)
console.log(obj1.birth.getFullYear()); // 1999(不影响原值)
console.log(obj1.fn()); // hello(函数独立)
console.log(obj2.fn()); // world
(3)第三方库(成熟稳定,推荐生产环境)
-
Lodash 库的
_.cloneDeep()(支持所有类型,处理循环引用)js// 安装:npm i lodash const _ = require("lodash"); const obj1 = { name: "张三", info: { age: 25 }, a: obj1 }; // 循环引用 const obj2 = _.cloneDeep(obj1); // 深拷贝,正常处理循环引用 obj2.info.age = 30; console.log(obj1.info.age); // 25 -
jQuery 库的
$.extend(true, {}, obj)(true 表示深拷贝)jsconst obj1 = { name: "张三", info: { age: 25 } }; const obj2 = $.extend(true, {}, obj1); // 深拷贝
3. 深拷贝的特点
- 优点:新旧数据完全独立,修改任意一方不会影响另一方,避免数据污染;
- 缺点:实现复杂(手动递归需处理多种特殊类型)、性能开销大(递归遍历所有层级);
- 适用场景:需要修改拷贝后的数据,且数据包含深层嵌套引用类型的场景(如表单提交、状态管理、复杂数据处理)。
四、深拷贝 vs 浅拷贝 核心对比
| 对比维度 | 浅拷贝(Shallow Copy) | 深拷贝(Deep Copy) |
|---|---|---|
| 拷贝层级 | 仅拷贝表层(第一层)数据 | 递归拷贝所有层级数据 |
| 引用类型处理 | 深层嵌套引用类型仅复制内存地址(共享) | 深层嵌套引用类型创建独立副本(不共享) |
| 数据独立性 | 深层数据共享,修改会相互影响 | 完全独立,修改互不影响 |
| 实现难度 | 简单(原生 API 即可实现) | 复杂(需处理特殊类型、循环引用) |
| 性能开销 | 小(仅遍历表层) | 大(递归遍历所有层级) |
| 适用场景 | 表层数据拷贝、无需修改深层数据 | 复杂嵌套数据拷贝、需要独立修改数据 |
| 常见实现 | 数组:slice、concat、[...arr];对象:Object.assign、{...obj} | JSON.parse (JSON.stringify ())、手动递归、_.cloneDeep () |
五、常见误区
- 认为
Object.assign是深拷贝 :Object.assign仅对第一层数据实现值拷贝,深层引用类型仍为引用传递,属于浅拷贝; - JSON 方式能处理所有数据:JSON 序列化无法处理函数、正则、循环引用、Symbol 等类型,仅适用于简单 JSON 数据;
- 原始类型需要深浅拷贝:原始类型赋值时直接传递值,不存在引用,无需区分深浅拷贝;
- 深拷贝一定优于浅拷贝:深拷贝性能开销大,若数据无深层嵌套,浅拷贝更高效,无需过度使用深拷贝。
总结
- 核心区别:是否拷贝深层嵌套的引用类型,决定数据是否独立;
- 原始类型无深浅拷贝之分,引用类型才需要区分;
- 浅拷贝:简单高效,适合表层数据,推荐
[...arr]/{...obj}/Object.assign; - 深拷贝:完全独立,适合复杂嵌套数据,简单场景用
JSON.parse(JSON.stringify()),生产环境推荐_.cloneDeep(); - 选型原则:根据数据结构选择,无需深层独立时优先浅拷贝,避免性能浪费。