JavaScript 深拷贝与浅拷贝完全指南
前言:从一段代码说起
让我们先看一段揭示拷贝本质的代码:
// 基本类型拷贝
let a = 1
let b = a
a = 2
console.log(b) // 输出:1(b 的值不受 a 后续变化影响)
// 引用类型拷贝
let obj = { age: 18 }
let obj2 = obj
obj.age = 20
console.log(obj2.age) // 输出:20(obj2 的属性随 obj 变化)
这段代码揭示了 JavaScript 中一个核心概念:基本类型是按值拷贝,而引用类型是按引用拷贝。当我们需要完整复制引用类型数据时,就涉及到浅拷贝和深拷贝的概念。
正文
1. 什么是浅拷贝与深拷贝?
浅拷贝(Shallow Copy)
- 定义:创建一个新对象,复制原始对象的顶层属性。如果属性是基本类型,则复制值;如果是引用类型,则复制内存地址(共享同一引用),简单来说就是修改原来的对象会影响到拷贝对象就是浅拷贝,相反,修改原来的对象不会影响到拷贝对象就是深拷贝
- 特点:
- 只复制对象的第一层
- 嵌套对象仍然是共享的
- 修改嵌套对象会影响原对象和拷贝对象
深拷贝(Deep Copy)
- 定义:创建一个新对象,递归复制原始对象的所有层级属性,完全切断与原始对象的引用关系
- 特点:
- 复制对象的所有层级
- 嵌套对象也是独立的副本
- 修改任何层级都不会影响原对象
2. 浅拷贝的实现方法
2.1 Object.create()
const original = { a: 1, b: { c: 2 } }
const copy = Object.create(original)
// 注意:这种方式创建的对象会以 original 作为原型
2.2 Object.assign()
const original = { a: 1, b: { c: 2 } }
const copy = Object.assign({}, original)
// 修改 copy.b.c 会影响 original.b.c
2.3 数组方法:concat() 和 slice()
const arr = [1, 2, { a: 3 }]
const copy1 = arr.concat()
const copy2 = arr.slice()
// 修改 copy1[2].a 会影响原数组
2.4 数组解构
const arr = [1, 2, { a: 3 }]
const copy = [...arr]
// 修改 copy[2].a 会影响原数组
2.5 toReversed() + reverse()
const arr = [1, 2, { a: 3 }]
const copy = arr.toReversed().reverse()
// ES2023 新方法,同样是浅拷贝
3. 深拷贝的实现方法
3.1 JSON 序列化法(最常用)
const original = { a: 1, b: { c: 2 } }
const copy = JSON.parse(JSON.stringify(original))
// 完全独立的副本
缺点:
- 无法处理特殊类型:
undefined→ 丢失function→ 丢失Symbol→ 丢失BigInt→ 报错
- 无法处理循环引用:
const obj = { a: 1 } obj.self = obj JSON.parse(JSON.stringify(obj)) // 报错 - 会丢失原型链信息
3.2 手动实现深拷贝
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型和 null/undefined
if (obj === null || typeof obj !== 'object') {
return obj
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj)
}
// 处理 Date 和 RegExp
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 创建新对象/数组
const cloneObj = new obj.constructor()
hash.set(obj, cloneObj)
// 递归拷贝属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key], hash)
}
}
return cloneObj
}
4. 如何选择拷贝方式?
| 场景 | 推荐方法 |
|---|---|
| 只需要第一层拷贝 | 浅拷贝方法 |
| 需要完全独立副本 | 深拷贝方法 |
| 简单对象,无特殊类型 | JSON.parse(JSON.stringify()) |
| 复杂对象,有函数/Symbol等 | 手动实现或 Lodash |
| 考虑性能的大对象 | 结构化克隆算法(如 MessageChannel) |
5. 性能考量
- 浅拷贝:性能高,时间复杂度 O(n)
- 深拷贝:性能较低,时间复杂度 O(n) + 递归开销
- JSON 方法:对于大对象性能较差(需要序列化和解析)
- 手动实现:可以通过 WeakMap 优化循环引用检测
结语
理解深浅拷贝是 JavaScript 开发者的必备技能。在实际开发中:
- 优先使用浅拷贝:当确定只需要第一层拷贝时
- 谨慎选择深拷贝方法:根据数据结构特点选择合适方案
- 处理循环引用:使用 WeakMap 或现成库函数
- 注意特殊类型:函数、Symbol 等需要特殊处理
记住这个简单的原则:当你需要完全独立的副本时,必须使用深拷贝。对于大多数日常需求,JSON.parse(JSON.stringify()) 已经足够,但在复杂场景下,考虑使用 Lodash 等库函数或自己实现健壮的深拷贝函数。
掌握这些知识后,你将能够:
- 避免意外的引用共享导致的 bug
- 在需要时创建完全独立的数据副本
- 在面试中游刃有余地回答相关问题
祝你在 JavaScript 的深拷贝与浅拷贝世界中探索愉快!🚀