JavaScript浅拷贝与深拷贝
目录
浅拷贝(Shallow Copy)
一、浅拷贝的定义
二、直接赋值 vs 浅拷贝
1. 直接赋值
2. 浅拷贝
三、数组的浅拷贝方法
1. slice()
2. concat()
3. 扩展运算符(...)
四、对象的浅拷贝方法
1. Object.assign()
2. 扩展运算符(...)
五、浅拷贝的局限性
六、总结
深拷贝(Deep Copy)
一、深拷贝的定义
二、深拷贝的常见实现方式
1. JSON.parse(JSON.stringify())
2. 递归手动实现
3. 第三方库(如 Lodash 的 _.cloneDeep())
4. MessageChannel(异步深拷贝)
三、深拷贝的边界条件处理
1. 循环引用
2. 特殊对象类型
四、总结
五、浅拷贝 vs 深拷贝对比
六、_.cloneDeep(value) 源码解析
1. 核心逻辑
2. 关键源码步骤
3. 核心特性
浅拷贝(Shallow Copy)
一、浅拷贝的定义
-
核心概念:创建一个新对象,仅复制原始对象的 第一层属性值。若属性为 基本类型,拷贝其值;若为 引用类型(如对象、数组),则拷贝其内存地址(共享同一引用)。
-
特点:
-
修改新对象的 引用类型属性 会影响原对象。
-
修改新对象的 基本类型属性 不会影响原对象。
-
二、直接赋值 vs 浅拷贝
1. 直接赋值
-
本质:将变量指向原对象的 内存地址,未创建新对象。
-
结果:新旧变量完全共享同一对象,任意修改均相互影响。
const obj = { a: 1, b: { c: 2 } }; const copy = obj; // 直接赋值 copy.a = 10; copy.b.c = 20; console.log(obj.a); // 10(基本类型也被修改) console.log(obj.b.c); // 20
2. 浅拷贝
-
本质:创建新对象,复制原对象的 第一层属性值。
-
结果:
-
基本类型属性:独立修改,互不影响。
-
引用类型属性:共享同一内存地址,修改相互影响。
const obj = { a: 1, b: { c: 2 } }; const copy = { ...obj }; // 浅拷贝(扩展运算符) copy.a = 10; // 修改基本类型属性 copy.b.c = 20; // 修改引用类型属性 console.log(obj.a); // 1(原对象未变) console.log(obj.b.c); // 20(原对象被修改)
-
三、数组的浅拷贝方法
1. slice()
const arr = [1, 2, { a: 3 }];
const copy = arr.slice(); // 浅拷贝数组
copy[0] = 10; // 修改基本类型元素
copy[2].a = 30; // 修改引用类型元素
console.log(arr[0]); // 1(原数组未变)
console.log(arr[2].a); // 30(原数组被修改)
2. concat()
const arr = [1, 2, { a: 3 }];
const copy = arr.concat(); // 浅拷贝数组
3. 扩展运算符(...
)
const arr = [1, 2, { a: 3 }];
const copy = [...arr]; // 浅拷贝数组
四、对象的浅拷贝方法
1. Object.assign()
const obj = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, obj); // 浅拷贝对象
copy.a = 10; // 修改基本类型属性
copy.b.c = 20; // 修改引用类型属性
console.log(obj.a); // 1(原对象未变)
console.log(obj.b.c); // 20(原对象被修改)
2. 扩展运算符(...
)
const obj = { a: 1, b: { c: 2 } };
const copy = { ...obj }; // 浅拷贝对象
五、浅拷贝的局限性
-
嵌套引用类型:浅拷贝无法处理多层级对象或数组中的引用类型属性。
const original = { a: 1, b: { c: 2 }, d: [3] }; const copy = { ...original }; copy.b.c = 20; // 原对象被修改 copy.d.push(4); // 原数组被修改 console.log(original.b.c); // 20 console.log(original.d); // [3, 4]
六、总结
-
浅拷贝:复制对象的第一层属性,适用于简单数据结构。
-
直接赋值:共享内存地址,无独立副本。
-
数组浅拷贝:
slice()
、concat()
、扩展运算符。 -
对象浅拷贝:
Object.assign()
、扩展运算符。 -
深层拷贝需求:需使用深拷贝方法。
深拷贝(Deep Copy)
一、深拷贝的定义
-
核心概念:创建一个新对象,递归复制原始对象的所有层级属性(包括嵌套对象和数组),新对象与原对象完全独立,修改互不影响。
-
特点:
-
完全独立内存:所有层级的引用类型属性均为新对象。
-
适用场景:复杂嵌套对象、需完全隔离数据的场景(如状态管理、数据快照)。
-
二、深拷贝的常见实现方式
1. JSON.parse(JSON.stringify())
-
原理:将对象序列化为 JSON 字符串,再解析为新的对象。
-
示例:
const obj = { a: 1, b: { c: 2 }, d: [3] }; const copy = JSON.parse(JSON.stringify(obj)); copy.b.c = 20; console.log(obj.b.c); // 2(原对象未变)
-
优点:简单快捷,适用于大多数 JSON 安全的数据。
-
缺点:
-
不支持特殊类型:忽略
undefined
、Symbol
、函数、循环引用。 -
破坏原型链:无法复制对象的构造函数和原型方法。
-
日期对象:会被转换为 ISO 字符串,反序列化为字符串而非 Date 对象。
-
2. 递归手动实现
-
原理:递归遍历对象属性,逐层复制基本类型和引用类型。
-
示例:
简易版:
function deepCopy(newObj, oldObj) {
debugger //类似于在代码中设置断点
for (let k in oldObj) {
// 处理数组的问题 一定先写数组再写对象 不能颠倒
if (oldObj[k] instanceof Array) {
newObj[k] = []
deepCopy(newObj[k], oldObj[k])
}
else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
}
else {
newObj[k] = oldObj[k]
}
}
}
升级版:
function deepClone(source, map = new WeakMap()) {
// 处理基本类型和 null
if (source === null || typeof source !== 'object') return source;
// 处理循环引用
if (map.has(source)) return map.get(source);
// 处理数组和对象
const target = Array.isArray(source) ? [] : {};
map.set(source, target);
// 递归复制属性
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = deepClone(source[key], map);
}
}
return target;
}
// 使用示例
const obj = { a: 1, b: { c: 2 }, d: [3] };
const copy = deepClone(obj);
copy.b.c = 20;
console.log(obj.b.c); // 2(原对象未变)
-
优点:
-
支持所有数据类型(需扩展代码处理
Date
、RegExp
、Map
、Set
等)。 -
可处理循环引用(通过 WeakMap 缓存)。
-
-
缺点:
-
实现复杂,需处理多种边界情况。
-
性能较低(递归遍历耗时)。
-
3. 第三方库(如 Lodash 的 _.cloneDeep()
)
-
原理:成熟的深拷贝实现,处理了各种数据类型和边界条件。
-
示例:
const _ = require('lodash'); const obj = { a: 1, b: { c: 2 }, d: [3] }; const copy = _.cloneDeep(obj); copy.b.c = 20; console.log(obj.b.c); // 2
-
优点:
-
功能全面,支持复杂类型(如
Date
、RegExp
、Map
、Set
)。 -
高性能且经过严格测试。
-
-
缺点:需引入外部库。
4. MessageChannel
(异步深拷贝)
-
原理:利用浏览器 API 的通信机制实现深拷贝。
-
示例:
function deepClone(obj) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }); } // 使用示例(异步) const obj = { a: 1, b: { c: 2 } }; deepClone(obj).then(copy => { copy.b.c = 20; console.log(obj.b.c); // 2 });
-
优点:可处理循环引用和部分特殊对象(如
Date
)。 -
缺点:
-
异步操作,使用不便。
-
无法复制函数、
undefined
等非序列化数据。 -
仅限浏览器环境。
-
三、深拷贝的边界条件处理
1. 循环引用
-
问题:对象属性间接或直接引用自身,导致递归无限循环。
-
解决:使用
WeakMap
缓存已拷贝对象。const obj = { a: 1 }; obj.self = obj; // 循环引用 const copy = deepClone(obj); // 正确处理
2. 特殊对象类型
-
处理方式:
-
Date
:创建新Date
实例。 -
RegExp
:复制正则表达式和标志。 -
Map
/Set
:递归复制其内容。
// 扩展递归函数处理 Date if (source instanceof Date) return new Date(source);
-
四、总结
方法 | 推荐场景 | 注意事项 |
---|---|---|
JSON 方法 | 简单 JSON 安全数据(无特殊类型) | 忽略函数、undefined 、循环引用 |
递归手动实现 | 需要完全控制深拷贝逻辑 | 需扩展处理特殊类型和循环引用 |
Lodash.cloneDeep | 生产环境,复杂数据类型 | 引入外部依赖 |
MessageChannel | 浏览器环境,支持部分特殊对象 | 异步操作,无法复制函数 |
最佳实践:
-
优先使用 Lodash 等成熟库:减少重复造轮子和潜在错误。
-
手动实现需谨慎:充分测试循环引用、特殊数据类型和性能。
-
避免滥用深拷贝:在数据隔离需求明确时使用,避免不必要的性能损耗。
五、浅拷贝 vs 深拷贝对比
特性 | 浅拷贝 | 深拷贝 |
---|---|---|
复制层级 | 仅复制第一层属性 | 递归复制所有层级属性 |
内存占用 | 低(共享引用类型属性) | 高(完全独立副本) |
性能 | 高(仅复制第一层) | 较低(递归遍历耗时) |
适用场景 | 简单对象或无需隔离引用属性的场景 | 需要完全隔离数据的场景 |
实现复杂度 | 低(简单复制) | 高(需处理循环引用、特殊类型) |
六、_.cloneDeep(value)
源码解析
1. 核心逻辑
_.cloneDeep
用于深拷贝任意类型的数据,包括对象、数组、Date
、RegExp
、Map
、Set
等。其核心实现依赖于递归遍历和类型判断。
2. 关键源码步骤
function cloneDeep(value) {
return baseClone(value, true /* deep */);
}
// 基础克隆函数(Lodash 内部实现)
function baseClone(value, isDeep, customizer, key, object, stack) {
// 使用栈跟踪循环引用
stack || (stack = new Stack());
// 处理已拷贝的对象(避免循环引用)
if (stack.has(value)) {
return stack.get(value);
}
let result;
// 根据类型选择克隆策略
if (isObject(value)) {
// 处理数组、对象、Map、Set、Date、RegExp 等
const tag = getTag(value);
switch (tag) {
case '[object Array]':
result = initCloneArray(value);
break;
case '[object Date]':
result = new Date(value.getTime());
break;
case '[object Map]':
result = new Map();
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, isDeep, customizer, key, value, stack));
});
break;
// 其他类型处理(如 Set、RegExp 等)
// ...
}
// 注册到栈中,处理循环引用
stack.set(value, result);
// 递归拷贝属性
copyProperties(value, result, isDeep, customizer, stack);
} else {
// 处理基本类型(直接返回)
result = value;
}
return result;
}
3. 核心特性
-
循环引用处理:使用
Stack
结构(内部基于数组或WeakMap
)记录已拷贝对象。 -
类型识别:通过
Object.prototype.toString.call(value)
获取精确类型。 -
递归拷贝:对嵌套结构逐层复制,确保深层次独立性。