前言
无论你在哪个平台学习 JavaScript,深浅拷贝都是绕不开的核心知识点。它直接关系到对象数据的修改安全,也是面试高频考点。本文将从 “拷贝的本质” 出发,清晰拆解浅拷贝与深拷贝的区别、实现方式及适用场景,帮你彻底搞懂这一基础概念。
一、浅拷贝:只拷贝对象第一层
浅拷贝的操作对象仅为引用类型(如对象、数组) 。它对对象第一层的处理规则如下:
- 若属性是基本数据类型(number、string 等) ,直接拷贝值,修改新对象不会影响原对象。
- 若属性是引用数据类型(对象、数组等) ,仅拷贝引用地址,新对象与原对象的嵌套引用指向同一内存地址,修改嵌套内容会相互影响。
浅拷贝实现方式
方式 1:Object.assign ()(ES5 方法)
Object.assign(target, source) 会将源对象(source)的可枚举属性复制到目标对象(target),返回目标对象。
// 原对象(包含基本类型和引用类型属性)
const originalObj = {
name: "张三", // 基本类型
info: { age: 20 } // 引用类型
};
// 目标对象(初始空对象)
const copyObj = {};
// 执行浅拷贝
Object.assign(copyObj, originalObj);
// 验证:修改基本类型属性(互不影响)
copyObj.name = "李四";
console.log(originalObj.name); // 输出 "张三"(原对象不变)
// 验证:修改引用类型属性(相互影响)
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 22(原对象被修改)
方式 2:扩展运算符 ...(ES6 方法)
扩展运算符可快速拷贝对象或数组的第一层,语法更简洁,是日常开发的首选。
// 1. 拷贝对象
const originalObj = { name: "张三", info: { age: 20 } };
const copyObj = { ...originalObj }; // 浅拷贝
// 验证引用类型关联
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 22(原对象被修改)
// 2. 拷贝数组
const originalArr = [1, { value: 10 }];
const copyArr = [...originalArr]; // 浅拷贝
// 验证引用类型关联
copyArr[1].value = 20;
console.log(originalArr[1].value); // 输出 20(原数组被修改)
方式 3:数组的 slice () 方法
slice(start, end) 用于截取数组片段,返回新数组,本质是对数组的浅拷贝(仅作用于数组)。
const originalArr = [{ name: "苹果" }, { name: "香蕉" }];
const copyArr = originalArr.slice(); // 不传参数时,拷贝整个数组
// 验证引用类型关联
copyArr[0].name = "西瓜";
console.log(originalArr[0].name); // 输出 "西瓜"(原数组被修改)
二、深拷贝:完全独立的新对象
深拷贝会递归拷贝对象的所有层级(包括嵌套的引用类型),最终生成一个与原对象完全独立的新对象。修改新对象的任何属性(无论层级),都不会影响原对象。
深拷贝实现方式(附完整代码)
方式 1:JSON.parse (JSON.stringify ())(简单场景适用)
通过 “先转 JSON 字符串,再转对象” 的方式实现深拷贝,无需额外依赖,但存在明显局限性。
const originalObj = {
name: "张三",
info: { age: 20 },
hobby: ["游戏", "运动"]
};
// 执行深拷贝
const copyObj = JSON.parse(JSON.stringify(originalObj));
// 验证:修改嵌套引用类型(互不影响)
copyObj.info.age = 22;
copyObj.hobby[0] = "阅读";
console.log(originalObj.info.age); // 输出 20(原对象不变)
console.log(originalObj.hobby[0]); // 输出 "游戏"(原对象不变)
⚠️ 局限性(必须注意) :
-
无法处理
function类型:会直接忽略函数属性const obj = { fn: () => console.log("hello") }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.fn); // 输出 undefined(函数被忽略) -
无法处理
undefined类型:会忽略值为undefined的属性const obj = { age: undefined }; const copy = JSON.parse(JSON.stringify(obj)); console.log(copy.age); // 输出 undefined(属性被忽略,copy 仅为 {}) -
无法处理循环引用:会直接报错(对象属性引用自身)
const obj = { name: "张三" }; obj.self = obj; // 循环引用:obj 的 self 属性指向自身 const copy = JSON.parse(JSON.stringify(obj)); // 报错:TypeError: Converting circular structure to JSON
方式 2:第三方库(生产环境首选)
开发中推荐使用成熟的第三方库实现深拷贝,兼容性好、无漏洞,常用库包括 lodash 和 jQuery。
以 lodash.cloneDeep 为例(最常用):
// 1. 先安装 lodash(npm 项目)
// npm install lodash
// 2. 引入并使用
const _ = require("lodash");
const originalObj = {
name: "张三",
info: { age: 20 },
fn: () => console.log("hello"), // 包含函数
self: null // 后续用于循环引用
};
originalObj.self = originalObj; // 循环引用
// 执行深拷贝(支持函数、循环引用)
const copyObj = _.cloneDeep(originalObj);
// 验证:完全独立
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 20(原对象不变)
console.log(copyObj.fn); // 输出 [Function: fn](函数被保留)
方式 3:手写深拷贝(面试高频考点)
自定义深拷贝函数可根据需求灵活扩展,核心是 “递归遍历 + 处理不同数据类型 + 解决循环引用”。
function deepClone(original) {
// 1. 存储已拷贝的对象,解决循环引用(用 WeakMap 避免内存泄漏)
const cache = new WeakMap();
// 2. 递归拷贝的核心函数
const _clone = (value) => {
// 情况 1:非对象类型(基本类型、null、undefined),直接返回值
if (value === null || typeof value !== "object") {
return value;
}
// 情况 2:已拷贝过的对象,直接返回缓存结果(解决循环引用)
if (cache.has(value)) {
return cache.get(value);
}
let result;
// 情况 3:数组类型
if (Array.isArray(value)) {
result = [];
cache.set(value, result); // 先缓存,再递归(避免循环引用)
value.forEach((item, index) => {
result[index] = _clone(item); // 递归拷贝数组元素
});
}
// 情况 4:普通对象类型(排除数组、Date、Map 等)
else if (Object.prototype.toString.call(value) === "[object Object]") {
result = {};
cache.set(value, result); // 先缓存,再递归
for (const key in value) {
// 只拷贝对象自身的可枚举属性(跳过原型链属性)
if (value.hasOwnProperty(key)) {
result[key] = _clone(value[key]); // 递归拷贝属性值
}
}
}
// 情况 5:扩展支持 Date 类型(可选,根据需求添加)
else if (value instanceof Date) {
result = new Date(value); // Date 对象直接新建
cache.set(value, result);
}
return result;
};
return _clone(original);
}
// 测试手写深拷贝
const originalObj = {
name: "张三",
info: { age: 20 },
time: new Date() // 包含 Date 类型
};
originalObj.self = originalObj; // 循环引用
const copyObj = deepClone(originalObj);
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 20(完全独立)
console.log(copyObj.time instanceof Date); // 输出 true(Date 类型保留)
三、深浅拷贝核心区别对比
| 对比维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝层级 | 仅拷贝对象第一层 | 递归拷贝所有层级 |
| 引用类型处理 | 拷贝引用地址,共享内存 | 拷贝值,完全独立内存 |
| 修改影响 | 嵌套引用类型修改会相互影响 | 任何修改都互不影响 |
| 实现复杂度 | 简单(无需递归) | 复杂(需递归 + 处理特殊类型) |
| 适用场景 | 简单对象、无嵌套引用类型 | 复杂对象、有嵌套引用类型 |
总结
深浅拷贝的核心差异在于 “是否处理嵌套引用类型”:
- 日常开发中,若对象仅一层结构,优先用 扩展运算符 实现浅拷贝,高效简洁;
- 若对象包含多层嵌套(如复杂表单、树形数据),推荐用 lodash.cloneDeep 实现深拷贝,避免手动写函数的漏洞;
- 面试时,需能手写基础深拷贝函数,并说明 “循环引用” 和 “特殊类型(Date、Map)” 的处理思路。
学习深浅拷贝不仅是掌握语法,更能帮你理解 JavaScript 中 “引用类型” 的内存机制,减少开发中的数据修改 bug。