一文搞懂JS深浅拷贝:从原理到工业级实现,再也不怕面试问
深浅拷贝是前端面试的必考题 ,也是实际开发中最容易踩坑的知识点之一。很多人都遇到过"修改一个对象,另一个对象莫名其妙跟着变"的问题,这背后就是深浅拷贝的原理在起作用。
本文将从最基础的存储机制讲起,带你彻底搞懂深浅拷贝的本质区别,然后一步步实现一个覆盖所有边界情况的工业级深拷贝函数,最后总结所有常见误区和最佳实践。
一、为什么会有深浅拷贝之分?
一切的根源都在于JavaScript的数据类型存储机制。
JavaScript的数据类型分为两大类:
- 基本类型 :
string、number、boolean、null、undefined、BigInt、Symbol - 引用类型 :
object、array、function、Date、RegExp、Map、Set等
它们在内存中的存储方式完全不同:
| 类型 | 存储位置 | 存储内容 | 拷贝行为 |
|---|---|---|---|
| 基本类型 | 栈内存 | 直接存储值本身 | 拷贝时创建一个完全独立的副本,修改互不影响 |
| 引用类型 | 堆内存 | 栈中只存储指向堆内存的地址 | 拷贝时默认只拷贝地址,新旧变量指向同一块堆内存,修改会互相影响 |
举个最简单的例子:
javascript
// 基本类型拷贝
let a = 10;
let b = a;
b = 20;
console.log(a); // 10 ✅ 互不影响
// 引用类型拷贝
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.name = '李四';
console.log(obj1.name); // '李四' ❌ 原对象被修改了!
这就是为什么我们需要深浅拷贝:当我们需要一个独立的引用类型副本时,默认的赋值操作无法满足需求。
二、什么是浅拷贝?
定义
浅拷贝只拷贝对象的第一层属性。
- 如果属性是基本类型,拷贝的就是基本类型的值
- 如果属性是引用类型,拷贝的就是引用地址
也就是说,浅拷贝后的对象,第一层是独立的,但深层的引用类型仍然和原对象共享同一块内存。
直观演示
javascript
const original = {
name: '张三', // 第一层基本类型
info: { // 第一层引用类型
address: '北京市'
}
};
// 浅拷贝
const shallowClone = { ...original };
// 修改第一层基本类型:互不影响
shallowClone.name = '李四';
console.log(original.name); // '张三' ✅
// 修改深层引用类型:原对象被修改
shallowClone.info.address = '上海市';
console.log(original.info.address); // '上海市' ❌
常见的浅拷贝实现方式
所有只拷贝第一层属性的方法都是浅拷贝,前端最常用的有以下几种:
1. 对象展开运算符 ...(推荐)
javascript
const clone = { ...original };
2. Object.assign()
javascript
const clone = Object.assign({}, original);
3. 数组展开运算符 ...
javascript
const arr = [1, 2, { a: 3 }];
const clone = [...arr];
4. 数组的 slice() 和 concat()
javascript
const arr = [1, 2, { a: 3 }];
const clone1 = arr.slice();
const clone2 = arr.concat();
⚠️ 面试重点:以上所有方法都是浅拷贝!很多新手误以为它们是深拷贝,这是最常见的面试坑。
三、什么是深拷贝?
定义
深拷贝会递归拷贝对象的所有层级属性。
- 无论属性是基本类型还是引用类型,都会创建一个全新的副本
- 新旧对象完全独立,修改任何属性都不会互相影响
直观演示
javascript
const original = {
name: '张三',
info: {
address: '北京市'
}
};
// 深拷贝
const deepClone = _.cloneDeep(original); // 使用Lodash的深拷贝
// 修改任何属性都不会影响原对象
deepClone.name = '李四';
deepClone.info.address = '上海市';
console.log(original.name); // '张三' ✅
console.log(original.info.address); // '北京市' ✅
常见的深拷贝实现方式
1. JSON.parse(JSON.stringify())(最简单但缺陷最多)
这是最容易想到的深拷贝方式,一行代码就能实现:
javascript
const clone = JSON.parse(JSON.stringify(original));
优点 :简单方便,不需要自己写逻辑 缺点:有非常多的局限性,会丢失或错误处理以下类型的数据:
- 丢失
undefined、function、Symbol类型的属性 Date对象会变成字符串RegExp、Error对象会变成空对象BigInt类型会直接报错- 无法处理循环引用(会直接抛出
RangeError)
反例演示:
javascript
const original = {
a: undefined,
b: () => console.log('hello'),
c: Symbol('id'),
d: new Date(),
e: /abc/g
};
const clone = JSON.parse(JSON.stringify(original));
console.log(clone);
// { d: "2026-05-13T08:00:00.000Z", e: {} } ❌ 大部分属性都丢失了
2. 递归实现(最灵活,可定制)
这是最核心的实现方式,也是面试中最常考的。我们将在下一节一步步实现一个工业级的深拷贝函数。
3. 第三方库(生产环境首选)
- Lodash
_.cloneDeep():最成熟、最稳定的深拷贝实现,覆盖了所有边界情况 - Immer:基于不可变数据的深拷贝,非常适合React等框架
四、从零实现一个工业级深拷贝函数
下面我们将一步步实现一个覆盖99%前端开发场景的深拷贝函数,解决所有常见的边界问题。
步骤1:基础版本(处理基本类型和普通对象/数组)
javascript
function deepClone(obj) {
// 处理基本类型和null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 创建新对象或数组
const cloneObj = Array.isArray(obj) ? [] : {};
// 遍历属性并递归拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
问题 :这个版本只能处理普通对象和数组,无法处理循环引用、Date、RegExp等特殊对象。
步骤2:处理循环引用
循环引用是指对象的属性间接或直接指向自身,比如:
javascript
const obj = {};
obj.self = obj; // 循环引用
基础版本遇到循环引用会无限递归,导致栈溢出。我们用WeakMap来缓存已拷贝的对象,解决这个问题:
javascript
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 如果已经拷贝过这个对象,直接返回缓存的副本
if (hash.has(obj)) {
return hash.get(obj);
}
const cloneObj = Array.isArray(obj) ? [] : {};
// 缓存当前对象
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
步骤3:处理特殊内置对象
基础版本会把Date、RegExp等特殊对象当作普通对象处理,导致拷贝错误。我们需要单独处理这些类型:
javascript
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 处理RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 处理Error
if (obj instanceof Error) {
const cloneErr = new Error(obj.message);
cloneErr.stack = obj.stack;
return cloneErr;
}
if (hash.has(obj)) {
return hash.get(obj);
}
const cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
步骤4:处理ES6+集合类型(Map和Set)
javascript
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Error) {
const cloneErr = new Error(obj.message);
cloneErr.stack = obj.stack;
return cloneErr;
}
// 处理Map
if (obj instanceof Map) {
const cloneMap = new Map();
hash.set(obj, cloneMap);
for (const [key, value] of obj) {
cloneMap.set(deepClone(key, hash), deepClone(value, hash));
}
return cloneMap;
}
// 处理Set
if (obj instanceof Set) {
const cloneSet = new Set();
hash.set(obj, cloneSet);
for (const value of obj) {
cloneSet.add(deepClone(value, hash));
}
return cloneSet;
}
if (hash.has(obj)) {
return hash.get(obj);
}
const cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
步骤5:处理Symbol类型的属性名
for...in循环不会遍历Symbol类型的键,我们需要单独获取并拷贝:
javascript
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Error) {
const cloneErr = new Error(obj.message);
cloneErr.stack = obj.stack;
return cloneErr;
}
if (obj instanceof Map) {
const cloneMap = new Map();
hash.set(obj, cloneMap);
for (const [key, value] of obj) {
cloneMap.set(deepClone(key, hash), deepClone(value, hash));
}
return cloneMap;
}
if (obj instanceof Set) {
const cloneSet = new Set();
hash.set(obj, cloneSet);
for (const value of obj) {
cloneSet.add(deepClone(value, hash));
}
return cloneSet;
}
if (hash.has(obj)) {
return hash.get(obj);
}
const cloneObj = Array.isArray(obj) ? [] : {};
hash.set(obj, cloneObj);
// 拷贝字符串键
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
// 拷贝Symbol键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const key of symbolKeys) {
cloneObj[key] = deepClone(obj[key], hash);
}
return cloneObj;
}
最终工业级版本
加上参数校验和注释,就是一个可以直接在生产环境中使用的深拷贝函数:
javascript
/**
* 工业级深拷贝函数
* @param {*} obj - 需要拷贝的对象
* @param {WeakMap} hash - 用于处理循环引用的缓存
* @returns {*} 拷贝后的新对象
*/
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型、null、undefined、BigInt、函数和Promise
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理特殊内置对象
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Error) {
const cloneErr = new Error(obj.message);
cloneErr.stack = obj.stack;
return cloneErr;
}
// 处理ES6集合类型
if (obj instanceof Map) {
const cloneMap = new Map();
hash.set(obj, cloneMap);
for (const [key, value] of obj) {
cloneMap.set(deepClone(key, hash), deepClone(value, hash));
}
return cloneMap;
}
if (obj instanceof Set) {
const cloneSet = new Set();
hash.set(obj, cloneSet);
for (const value of obj) {
cloneSet.add(deepClone(value, hash));
}
return cloneSet;
}
// 处理TypedArray
if (obj instanceof ArrayBuffer) return obj.slice();
if (ArrayBuffer.isView(obj)) {
return new obj.constructor(obj.buffer.slice(), obj.byteOffset, obj.length);
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 创建新对象(保留原对象的原型链)
const cloneObj = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
hash.set(obj, cloneObj);
// 拷贝所有可枚举属性(包括字符串键和Symbol键)
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloneObj[key] = deepClone(obj[key], hash);
}
}
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const key of symbolKeys) {
cloneObj[key] = deepClone(obj[key], hash);
}
return cloneObj;
}
五、深浅拷贝核心区别对比表
| 对比维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝深度 | 只拷贝第一层属性 | 递归拷贝所有层级属性 |
| 引用类型处理 | 只拷贝引用地址,共享同一块内存 | 创建全新的副本,完全独立 |
| 修改影响 | 修改深层属性会影响原对象 | 修改任何属性都不会影响原对象 |
| 性能 | 快,资源消耗小 | 慢,资源消耗大,层级越深越明显 |
| 实现难度 | 简单,一行代码即可 | 复杂,需要处理大量边界情况 |
| 适用场景 | 对象只有一层基本类型属性 | 对象包含多层引用类型,需要完全独立的副本 |
六、99%的前端都踩过的坑
坑1:展开运算符是深拷贝
错! 展开运算符只拷贝第一层属性,深层引用类型仍然共享。这是最常见的误区。
坑2:JSON.parse(JSON.stringify())是万能的
错! 它会丢失undefined、function、Symbol,错误处理Date、RegExp,无法处理循环引用。
坑3:所有场景都应该用深拷贝
错! 深拷贝性能很差,对于只有一层基本类型的对象,浅拷贝完全足够,而且性能更好。
坑4:深拷贝可以拷贝一切
错! 函数的闭包、作用域,Promise的状态都是无法被拷贝的。所有主流库的深拷贝都会直接返回原函数和原Promise。
七、最佳实践
- 简单场景用浅拷贝 :如果对象只有一层基本类型属性,优先使用展开运算符
... - 复杂场景用第三方库 :生产环境中优先使用Lodash的
_.cloneDeep(),它经过了无数项目的验证 - 不要自己写深拷贝:除非是面试需要,否则不要在生产环境中使用自己写的深拷贝函数,很难覆盖所有边界情况
- React/Vue中更新状态:必须创建新的对象引用,这时候浅拷贝通常就足够了
八、总结
- 深浅拷贝的本质区别是对引用类型的拷贝深度不同
- 浅拷贝只拷贝第一层,深层引用共享内存;深拷贝递归拷贝所有层级,完全独立
JSON.parse(JSON.stringify())有很多缺陷,只能用于简单场景- 工业级深拷贝需要处理循环引用、特殊内置对象、ES6集合类型、Symbol属性等边界情况
- 生产环境优先使用Lodash的
_.cloneDeep(),不要重复造轮子
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!