深拷贝的实现思路深度解析
深拷贝的实现思路深度解析
深入理解 JavaScript 深拷贝的实现原理,掌握递归拷贝、类型判断、循环引用处理等核心技术。
一、背景与问题
浅拷贝只复制第一层,嵌套对象仍是引用:
1
2
3
4
5
const obj = { user: { name: 'Alice' } };
const shallowCopy = { ...obj };
shallowCopy.user.name = 'Bob';
console.log(obj.user.name); // 'Bob' - 原对象被修改!
深拷贝:递归复制所有层级,创建完全独立的副本。
1
2
3
4
5
const obj = { user: { name: 'Alice' } };
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.user.name = 'Bob';
console.log(obj.user.name); // 'Alice' - 原对象不受影响
二、核心概念与定义
2.1 深拷贝的定义
递归复制对象的所有层级属性,使新旧对象完全独立:
1
2
3
4
5
6
7
8
9
10
原对象 深拷贝后
┌─────────────┐ ┌─────────────┐
│ name: 'Alice'│ ──────────→ │ name: 'Alice'│
│ info: ──────┼──┐ │ info: ──────┼──┐
└─────────────┘ │ └─────────────┘ │
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│city:'BJ' │ ────────────→ │city:'BJ' │
└──────────┘ (新对象) └──────────┘
2.2 深拷贝需要处理的数据类型
| 类型 | 处理方式 |
|---|---|
| 基本类型 | 直接返回 |
| Object | 递归复制属性 |
| Array | 递归复制元素 |
| Date | new Date(value) |
| RegExp | new RegExp(source, flags) |
| Map | new Map(entries) |
| Set | new Set(values) |
| Function | 直接引用(或重新创建) |
| Symbol | 直接引用(或 Symbol(description)) |
| null/undefined | 直接返回 |
三、最小示例
3.1 JSON 方法(最简单)
1
2
3
4
5
6
7
8
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
const obj = { a: 1, b: { c: 2 } };
const copy = deepClone(obj);
copy.b.c = 3;
console.log(obj.b.c); // 2
局限性:
- 无法处理 Function、Symbol、undefined
- 无法处理循环引用
- Date 会变成字符串
- RegExp 会变成空对象
3.2 递归实现(基础版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function deepClone(obj) {
// 基本类型直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 数组
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
// 对象
const copy = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
四、核心知识点拆解
4.1 类型判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 获取精确类型
function getType(value) {
const type = typeof value;
// 基本类型
if (type !== 'object') {
return type;
}
// null
if (value === null) {
return 'null';
}
// 使用 Object.prototype.toString
const toString = Object.prototype.toString.call(value);
const map = {
'[object Object]': 'object',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Map]': 'map',
'[object Set]': 'set',
'[object WeakMap]': 'weakmap',
'[object WeakSet]': 'weakset',
'[object Function]': 'function',
'[object Symbol]': 'symbol',
'[object BigInt]': 'bigint',
'[object Arguments]': 'arguments'
};
return map[toString] || 'unknown';
}
// 使用
console.log(getType({})); // 'object'
console.log(getType([])); // 'array'
console.log(getType(new Date())); // 'date'
console.log(getType(null)); // 'null'
4.2 特殊类型处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function deepClone(obj, hash = new WeakMap()) {
// null 或非对象
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 循环引用检查
if (hash.has(obj)) {
return hash.get(obj);
}
const type = getType(obj);
let copy;
switch (type) {
case 'array':
copy = [];
hash.set(obj, copy);
for (let i = 0; i < obj.length; i++) {
copy[i] = deepClone(obj[i], hash);
}
break;
case 'date':
copy = new Date(obj);
break;
case 'regexp':
copy = new RegExp(obj.source, obj.flags);
break;
case 'map':
copy = new Map();
hash.set(obj, copy);
obj.forEach((value, key) => {
copy.set(deepClone(key, hash), deepClone(value, hash));
});
break;
case 'set':
copy = new Set();
hash.set(obj, copy);
obj.forEach(value => {
copy.add(deepClone(value, hash));
});
break;
case 'object':
default:
copy = {};
hash.set(obj, copy);
// 复制 Symbol 属性
const allKeys = [
...Object.keys(obj),
...Object.getOwnPropertySymbols(obj)
];
for (const key of allKeys) {
copy[key] = deepClone(obj[key], hash);
}
break;
}
return copy;
}
4.3 循环引用处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 问题:循环引用导致无限递归
const obj = { a: 1 };
obj.self = obj; // 循环引用
// JSON 方法会报错
JSON.parse(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON
// 解决:使用 WeakMap 记录已拷贝对象
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查是否已拷贝
if (hash.has(obj)) {
return hash.get(obj); // 返回已拷贝的副本
}
const copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy); // 记录映射关系
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key], hash);
}
}
return copy;
}
// 测试
const copy = deepClone(obj);
console.log(copy.self === copy); // true
4.4 为什么用 WeakMap?
1
2
3
4
5
6
7
8
9
10
11
12
13
// WeakMap vs Map
// Map:强引用,阻止垃圾回收
const map = new Map();
let obj = { a: 1 };
map.set(obj, 'value');
obj = null; // obj 不会被回收,因为 map 还引用它
// WeakMap:弱引用,不阻止垃圾回收
const weakMap = new WeakMap();
let obj = { a: 1 };
weakMap.set(obj, 'value');
obj = null; // obj 可以被回收
优势:
- 不阻止垃圾回收
- 键必须是对象(恰好用于记录对象映射)
- 不需要手动清理
五、实战案例
5.1 完整深拷贝实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function deepClone(source, hash = new WeakMap()) {
// 基本类型、null、undefined
if (source === null || source === undefined) {
return source;
}
// 基本包装类型
if (typeof source !== 'object') {
return source;
}
// 循环引用
if (hash.has(source)) {
return hash.get(source);
}
// 获取类型
const type = Object.prototype.toString.call(source);
let clone;
switch (type) {
case '[object Date]':
return new Date(source);
case '[object RegExp]':
return new RegExp(source.source, source.flags);
case '[object Map]':
clone = new Map();
hash.set(source, clone);
source.forEach((value, key) => {
clone.set(deepClone(key, hash), deepClone(value, hash));
});
return clone;
case '[object Set]':
clone = new Set();
hash.set(source, clone);
source.forEach(value => {
clone.add(deepClone(value, hash));
});
return clone;
case '[object Array]':
clone = [];
hash.set(source, clone);
for (let i = 0; i < source.length; i++) {
clone[i] = deepClone(source[i], hash);
}
return clone;
case '[object Object]':
clone = Object.create(Object.getPrototypeOf(source));
hash.set(source, clone);
// 复制所有自有属性(包括 Symbol)
const keys = Reflect.ownKeys(source);
for (const key of keys) {
clone[key] = deepClone(source[key], hash);
}
return clone;
case '[object Function]':
// 函数通常不拷贝,直接返回引用
return source;
default:
return source;
}
}
5.2 带选项的深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function deepClone(source, options = {}) {
const {
depth = Infinity, // 最大深度
ignoreKeys = [], // 忽略的键
customizer = null // 自定义拷贝函数
} = options;
function clone(value, currentDepth, hash) {
// 深度限制
if (currentDepth > depth) {
return value;
}
// 基本类型
if (value === null || typeof value !== 'object') {
return value;
}
// 循环引用
if (hash.has(value)) {
return hash.get(value);
}
// 自定义拷贝
if (customizer) {
const result = customizer(value);
if (result !== undefined) {
return result;
}
}
const copy = Array.isArray(value) ? [] : {};
hash.set(value, copy);
for (const key in value) {
if (value.hasOwnProperty(key) && !ignoreKeys.includes(key)) {
copy[key] = clone(value[key], currentDepth + 1, hash);
}
}
return copy;
}
return clone(source, 0, new WeakMap());
}
// 使用
const copy = deepClone(obj, {
depth: 3,
ignoreKeys: ['password'],
customizer: (value) => {
if (value instanceof Date) {
return new Date(value);
}
}
});
六、底层原理
6.1 递归与栈溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 深层嵌套可能导致栈溢出
const obj = {};
let current = obj;
for (let i = 0; i < 100000; i++) {
current.nested = {};
current = current.nested;
}
deepClone(obj); // RangeError: Maximum call stack size exceeded
// 解决:使用迭代代替递归
function deepCloneIterative(source) {
const root = {};
const stack = [{ source, target: root }];
const hash = new WeakMap();
hash.set(source, root);
while (stack.length > 0) {
const { source, target } = stack.pop();
for (const key in source) {
if (source.hasOwnProperty(key)) {
const value = source[key];
if (value === null || typeof value !== 'object') {
target[key] = value;
} else if (hash.has(value)) {
target[key] = hash.get(value);
} else {
target[key] = Array.isArray(value) ? [] : {};
hash.set(value, target[key]);
stack.push({ source: value, target: target[key] });
}
}
}
}
return root;
}
6.2 原型链处理
1
2
3
4
5
6
7
8
9
10
11
12
// 是否保持原型链?
const obj = Object.create({ proto: 'value' });
obj.own = 'own';
// 方式一:不保持原型链
const copy1 = { ...obj };
console.log(copy1.proto); // undefined
// 方式二:保持原型链
const copy2 = Object.create(Object.getPrototypeOf(obj));
Object.assign(copy2, obj);
console.log(copy2.proto); // 'value'
七、高频面试题解析
Q1:JSON.parse(JSON.stringify()) 的局限性?
答案:
- 无法处理 Function、Symbol、undefined(会丢失)
- 无法处理循环引用(会报错)
- Date 变成字符串
- RegExp 变成空对象
- NaN、Infinity 变成 null
- 会忽略不可枚举属性
1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
fn: () => {}, // 丢失
sym: Symbol('s'), // 丢失
undef: undefined, // 丢失
date: new Date(), // 变成字符串
regex: /test/g, // 变成空对象 {}
nan: NaN, // 变成 null
infinity: Infinity // 变成 null
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy);
// { date: "2026-05-05T00:00:00.000Z", regex: {}, nan: null, infinity: null }
Q2:如何处理循环引用?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用 WeakMap 记录已拷贝对象
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (hash.has(obj)) {
return hash.get(obj);
}
const copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key], hash);
}
}
return copy;
}
Q3:structuredClone 是什么?
答案:浏览器原生深拷贝 API,支持更多类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 浏览器原生方法
const obj = {
date: new Date(),
map: new Map([['a', 1]]),
set: new Set([1, 2, 3]),
regex: /test/g,
nested: { a: 1 }
};
const copy = structuredClone(obj);
// 支持的类型
// ✅ Object, Array, Date, RegExp, Map, Set, ArrayBuffer, TypedArray
// ❌ Function, Symbol, DOM 节点
// 也支持循环引用
const circular = { self: null };
circular.self = circular;
const copy2 = structuredClone(circular);
console.log(copy2.self === copy2); // true
Q4:深拷贝的性能优化?
1
2
3
4
5
6
7
8
9
10
11
// 1. 浅层对象用浅拷贝
function smartClone(obj, depth = 0) {
if (depth === 0 || isShallow(obj)) {
return { ...obj };
}
return deepClone(obj);
}
// 2. 使用迭代代替递归(避免栈溢出)
// 3. 缓存已拷贝对象(循环引用)
// 4. 跳过不需要拷贝的属性
八、总结与扩展
核心要点
- 类型判断:使用
Object.prototype.toString精确判断类型 - 递归拷贝:遍历所有属性,递归处理嵌套对象
- 循环引用:使用 WeakMap 记录已拷贝对象
- 特殊类型:Date、RegExp、Map、Set 需要特殊处理
方法选择指南
| 场景 | 推荐方法 |
|---|---|
| 简单对象(无特殊类型) | JSON.parse(JSON.stringify()) |
| 复杂对象(现代浏览器) | structuredClone() |
| 需要自定义处理 | 手写递归函数 |
| 生产环境 | Lodash _.cloneDeep() |
扩展阅读
- structuredClone:HTML 标准原生 API
- Lodash.cloneDeep:生产级深拷贝实现
- Immer:不可变数据操作,内部使用深拷贝
- MessageChannel:通过 postMessage 实现深拷贝
深拷贝是 JavaScript 中的核心技能,理解其实现原理有助于正确处理复杂数据结构。
本文由作者按照 CC BY 4.0 进行授权