手写完整深拷贝函数深度解析
从零手写支持多种数据类型、循环引用、Symbol的完整深拷贝函数,深入理解引用类型复制原理
手写完整深拷贝函数深度解析
一句话概括
深拷贝是递归复制所有层级属性的技术,核心难点在于处理循环引用、特殊对象类型和Symbol属性,是面试手写题的必考项。
背景
在前端面试中,”手写深拷贝”是最高频的手写题之一,原因有三:
- 考察全面性:涉及类型判断、递归、数据结构、边界处理
- 实战价值高:工作中经常需要复制复杂对象
- 区分度高:能区分初级和中级前端
常见面试追问:
- “怎么处理循环引用?”
- “Symbol属性怎么拷贝?”
- “Date、RegExp、Map、Set怎么处理?”
- “和JSON.parse(JSON.stringify())有什么区别?”
本文将逐步实现一个生产级别的深拷贝函数。
概念与定义
深拷贝 vs 浅拷贝
1
2
3
4
5
6
7
8
9
10
11
const original = { a: 1, b: { c: 2 } };
// 浅拷贝:只复制第一层
const shallow = { ...original };
shallow.b.c = 100;
console.log(original.b.c); // 100(原对象被影响)
// 深拷贝:递归复制所有层级
const deep = deepClone(original);
deep.b.c = 100;
console.log(original.b.c); // 2(原对象不受影响)
为什么JSON方法不够用?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {
date: new Date(),
regex: /test/g,
fn: () => {},
undef: undefined,
sym: Symbol('key'),
big: BigInt(123),
map: new Map([['a', 1]]),
set: new Set([1, 2, 3])
};
const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned);
// {
// date: "2026-05-08T...", // 变成字符串
// regex: {}, // 变成空对象
// map: {} // 变成空对象
// }
// fn、undef、sym、big、set 全部丢失!
JSON方法的局限:
- 无法处理
undefined、Symbol、BigInt、Function Date变成字符串,RegExp变成空对象Map、Set、Error等特殊对象丢失- 无法处理循环引用(报错)
- 丢失原型链
最小示例
基础版深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepClone(obj) {
// null 或非对象类型,直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 数组或对象
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
// 测试
const original = { a: 1, b: { c: 2, d: [3, 4] } };
const cloned = deepClone(original);
cloned.b.d[0] = 100;
console.log(original.b.d[0]); // 3(不受影响)
核心知识点拆解
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
// 可遍历的对象类型
const iterableTypes = [
'[object Object]',
'[object Array]',
'[object Map]',
'[object Set]',
'[object Arguments]'
];
// 特殊对象类型(需要特殊处理)
const specialTypes = {
'[object Date]': (obj) => new Date(obj),
'[object RegExp]': (obj) => new RegExp(obj.source, obj.flags),
'[object Error]': (obj) => new obj.constructor(obj.message),
'[object ArrayBuffer]': (obj) => obj.slice(0),
'[object DataView]': (obj) => new DataView(obj.buffer.slice(0)),
};
// 不需要拷贝的类型
const noCopyTypes = [
'[object Boolean]', // new Boolean()
'[object Number]', // new Number()
'[object String]', // new String()
'[object Function]', // 函数通常共享
'[object Promise]',
'[object WeakMap]',
'[object WeakSet]'
];
2. 循环引用处理
问题:对象属性引用自身或祖先,导致无限递归。
1
2
3
4
const obj = { a: 1 };
obj.self = obj; // 循环引用
deepClone(obj); // 无限递归 → 栈溢出
解决方案:用Map/WeakMap记录已拷贝的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj, cache = new Map()) {
// 检查缓存
if (cache.has(obj)) {
return cache.get(obj);
}
// ...拷贝逻辑
// 存入缓存
cache.set(obj, clone);
// 递归拷贝属性
for (const key in obj) {
clone[key] = deepClone(obj[key], cache);
}
return clone;
}
3. Symbol属性处理
Symbol作为属性键时,for...in和Object.keys()无法获取。
1
2
3
4
5
6
7
8
9
10
11
const obj = {};
const sym = Symbol('key');
obj[sym] = 'symbol value';
for (const key in obj) {
console.log(key); // 无输出
}
// 获取Symbol属性
Object.getOwnPropertySymbols(obj); // [Symbol(key)]
Reflect.ownKeys(obj); // 包含Symbol的所有属性
4. Map和Set的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
// Map
const map = new Map([['a', 1], ['b', 2]]);
const clonedMap = new Map();
for (const [key, value] of map) {
clonedMap.set(deepClone(key), deepClone(value));
}
// Set
const set = new Set([1, 2, { a: 3 }]);
const clonedSet = new Set();
for (const value of set) {
clonedSet.add(deepClone(value));
}
5. 原型链继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const person = new Person('Alice');
// 普通深拷贝会丢失原型
const cloned = deepClone(person);
cloned.sayHi; // undefined
// 需要保持原型链
const clonedWithProto = Object.assign(
Object.create(Object.getPrototypeOf(person)),
deepClone(person)
);
完整实现
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* 完整深拷贝函数
* 支持:基本类型、数组、对象、Map、Set、Date、RegExp、Error、ArrayBuffer、Symbol、循环引用
*/
function deepClone(obj, cache = new WeakMap()) {
// ========== 1. 基本类型和null ==========
if (obj === null || typeof obj !== 'object') {
return obj;
}
// ========== 2. 检查缓存(处理循环引用)==========
if (cache.has(obj)) {
return cache.get(obj);
}
// ========== 3. 获取类型标记 ==========
const type = Object.prototype.toString.call(obj);
// ========== 4. 特殊对象类型处理 ==========
// Date
if (type === '[object Date]') {
return new Date(obj);
}
// RegExp
if (type === '[object RegExp]') {
const cloned = new RegExp(obj.source, obj.flags);
cloned.lastIndex = obj.lastIndex;
return cloned;
}
// Error
if (type === '[object Error]') {
return new obj.constructor(obj.message);
}
// ArrayBuffer
if (type === '[object ArrayBuffer]') {
return obj.slice(0);
}
// TypedArray
if (ArrayBuffer.isView(obj)) {
return new obj.constructor(obj.buffer.slice(0));
}
// DataView
if (type === '[object DataView]') {
return new DataView(obj.buffer.slice(0));
}
// ========== 5. 不需要深拷贝的类型 ==========
if (type === '[object Function]') {
// 函数通常共享,返回原引用
return obj;
}
// ========== 6. 可遍历的容器类型 ==========
let clone;
// Map
if (type === '[object Map]') {
clone = new Map();
cache.set(obj, clone);
obj.forEach((value, key) => {
clone.set(deepClone(key, cache), deepClone(value, cache));
});
return clone;
}
// Set
if (type === '[object Set]') {
clone = new Set();
cache.set(obj, clone);
obj.forEach(value => {
clone.add(deepClone(value, cache));
});
return clone;
}
// ========== 7. 数组和普通对象 ==========
// 获取原型,创建新对象
const proto = Object.getPrototypeOf(obj);
clone = Array.isArray(obj) ? [] : Object.create(proto);
cache.set(obj, clone);
// ========== 8. 拷贝所有属性(包含Symbol)==========
// 获取所有自有属性(包括Symbol和不可枚举属性)
const keys = Reflect.ownKeys(obj);
for (const key of keys) {
// 跳过不可写的属性(可选)
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor && !descriptor.writable && !descriptor.configurable) {
Object.defineProperty(clone, key, descriptor);
continue;
}
clone[key] = deepClone(obj[key], cache);
}
return clone;
}
测试用例
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// ========== 测试1:基本类型 ==========
console.log(deepClone(123)); // 123
console.log(deepClone('hello')); // 'hello'
console.log(deepClone(null)); // null
console.log(deepClone(undefined)); // undefined
console.log(deepClone(Symbol('a'))); // Symbol(a)(Symbol是唯一的,无法真正复制)
// ========== 测试2:数组和对象 ==========
const arr = [1, [2, 3], { a: 4 }];
const clonedArr = deepClone(arr);
clonedArr[1][0] = 100;
console.log(arr[1][0]); // 2(不受影响)
const obj = { a: 1, b: { c: 2 } };
const clonedObj = deepClone(obj);
clonedObj.b.c = 100;
console.log(obj.b.c); // 2(不受影响)
// ========== 测试3:循环引用 ==========
const circular = { a: 1 };
circular.self = circular;
circular.nested = { parent: circular };
const clonedCircular = deepClone(circular);
console.log(clonedCircular.self === clonedCircular); // true
console.log(clonedCircular.nested.parent === clonedCircular); // true
console.log(clonedCircular !== circular); // true
// ========== 测试4:特殊对象 ==========
const date = new Date('2026-05-08');
const clonedDate = deepClone(date);
console.log(clonedDate.getTime() === date.getTime()); // true
console.log(clonedDate !== date); // true
const regex = /test/gi;
const clonedRegex = deepClone(regex);
console.log(clonedRegex.source); // 'test'
console.log(clonedRegex.flags); // 'gi'
clonedRegex.lastIndex = 10;
console.log(clonedRegex.lastIndex); // 10
console.log(regex.lastIndex); // 0(不受影响)
// ========== 测试5:Map和Set ==========
const map = new Map([['a', 1], [{ b: 2 }, 3]]);
const clonedMap = deepClone(map);
const mapKey = [...map.keys()][1];
const clonedMapKey = [...clonedMap.keys()][1];
console.log(clonedMapKey !== mapKey); // true(key也被深拷贝)
const set = new Set([1, { a: 2 }]);
const clonedSet = deepClone(set);
const setVal = [...set.values()][1];
const clonedSetVal = [...clonedSet.values()][1];
console.log(clonedSetVal !== setVal); // true
// ========== 测试6:Symbol属性 ==========
const sym = Symbol('key');
const objWithSym = { [sym]: 'symbol value', normal: 'normal value' };
const clonedSym = deepClone(objWithSym);
console.log(clonedSym[sym]); // 'symbol value'
console.log(Object.getOwnPropertySymbols(clonedSym)); // [Symbol(key)]
// ========== 测试7:原型链 ==========
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
return `Hi, I'm ${this.name}`;
};
const person = new Person('Alice');
const clonedPerson = deepClone(person);
console.log(clonedPerson.name); // 'Alice'
console.log(clonedPerson.sayHi()); // "Hi, I'm Alice"
console.log(clonedPerson instanceof Person); // true
// ========== 测试8:TypedArray和ArrayBuffer ==========
const buffer = new ArrayBuffer(8);
const view = new Int32Array(buffer);
view[0] = 123;
const clonedBuffer = deepClone(buffer);
const clonedView = deepClone(view);
console.log(clonedView[0]); // 123
clonedView[0] = 456;
console.log(view[0]); // 123(不受影响)
// ========== 测试9:函数 ==========
const fn = function(a, b) { return a + b; };
fn.customProp = 'test';
const clonedFn = deepClone(fn);
console.log(clonedFn(1, 2)); // 3(函数共享)
console.log(clonedFn.customProp); // 'test'
// ========== 测试10:性能测试 ==========
const largeObj = {};
for (let i = 0; i < 10000; i++) {
largeObj[`key${i}`] = { nested: { value: i } };
}
console.time('deepClone');
const clonedLarge = deepClone(largeObj);
console.timeEnd('deepClone'); // 约 50-100ms
实战案例
案例1:实现lodash.cloneDeep的核心逻辑
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// lodash的cloneDeep核心实现简化版
function cloneDeep(value) {
return baseClone(value, new WeakMap());
}
function baseClone(value, cache) {
if (value === null || typeof value !== 'object') {
return value;
}
if (cache.has(value)) {
return cache.get(value);
}
const type = Object.prototype.toString.call(value);
// 根据类型分发
const handlers = {
'[object Object]': () => cloneObject(value, cache),
'[object Array]': () => cloneArray(value, cache),
'[object Map]': () => cloneMap(value, cache),
'[object Set]': () => cloneSet(value, cache),
'[object Date]': () => new Date(value),
'[object RegExp]': () => cloneRegExp(value),
'[object Error]': () => new value.constructor(value.message),
};
const handler = handlers[type];
if (handler) {
const result = handler();
cache.set(value, result);
return result;
}
return value;
}
function cloneObject(obj, cache) {
const clone = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, clone);
for (const key of Reflect.ownKeys(obj)) {
clone[key] = baseClone(obj[key], cache);
}
return clone;
}
function cloneArray(arr, cache) {
const clone = [];
cache.set(arr, clone);
for (let i = 0; i < arr.length; i++) {
clone[i] = baseClone(arr[i], cache);
}
return clone;
}
function cloneMap(map, cache) {
const clone = new Map();
cache.set(map, clone);
map.forEach((value, key) => {
clone.set(baseClone(key, cache), baseClone(value, cache));
});
return clone;
}
function cloneSet(set, cache) {
const clone = new Set();
cache.set(set, clone);
set.forEach(value => {
clone.add(baseClone(value, cache));
});
return clone;
}
function cloneRegExp(regex) {
const cloned = new RegExp(regex.source, regex.flags);
cloned.lastIndex = regex.lastIndex;
return cloned;
}
案例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
65
66
67
68
69
70
71
72
73
function cloneDeepWithOptions(value, options = {}) {
const {
ignoreKeys = [], // 忽略的属性名
ignoreTypes = [], // 忽略的类型
maxDepth = Infinity, // 最大深度
customizer = null, // 自定义拷贝函数
} = options;
return baseCloneWithOptions(value, {
cache: new WeakMap(),
depth: 0,
ignoreKeys: new Set(ignoreKeys),
ignoreTypes: new Set(ignoreTypes),
maxDepth,
customizer,
});
}
function baseCloneWithOptions(value, context) {
const { cache, depth, ignoreKeys, ignoreTypes, maxDepth, customizer } = context;
// 自定义处理
if (customizer) {
const result = customizer(value);
if (result !== undefined) return result;
}
// 基本类型
if (value === null || typeof value !== 'object') {
return value;
}
// 检查缓存
if (cache.has(value)) {
return cache.get(value);
}
// 深度限制
if (depth >= maxDepth) {
return value;
}
// 类型过滤
const type = Object.prototype.toString.call(value);
if (ignoreTypes.has(type)) {
return value;
}
const clone = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
cache.set(value, clone);
for (const key of Reflect.ownKeys(value)) {
if (ignoreKeys.has(key)) continue;
clone[key] = baseCloneWithOptions(value[key], { ...context, depth: depth + 1 });
}
return clone;
}
// 使用示例
const config = {
sensitive: 'password123',
cache: new Map(),
deep: { nested: { value: 1 } }
};
const cloned = cloneDeepWithOptions(config, {
ignoreKeys: ['sensitive'], // 忽略敏感字段
maxDepth: 3, // 最大深度
});
console.log(cloned.sensitive); // undefined
console.log(cloned.cache); // Map {}
底层原理
WeakMap vs Map
深拷贝中用WeakMap而非Map存储缓存,原因:
1
2
3
4
5
6
7
8
9
10
11
// Map:强引用,阻止垃圾回收
const map = new Map();
let obj = { a: 1 };
map.set(obj, 'cached');
obj = null; // obj仍然存在于map中,无法被GC
// WeakMap:弱引用,不阻止垃圾回收
const weakMap = new WeakMap();
obj = { a: 1 };
weakMap.set(obj, 'cached');
obj = null; // obj可以被GC回收
好处:
- 防止内存泄漏
- 当原对象被销毁,缓存自动释放
递归 vs 迭代
递归实现在深度很大时会栈溢出:
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
// 递归版:可能栈溢出
function deepCloneRecursive(obj) {
if (typeof obj !== 'object') return obj;
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
clone[key] = deepCloneRecursive(obj[key]);
}
return clone;
}
// 迭代版:使用栈
function deepCloneIterative(obj) {
if (typeof obj !== 'object') return obj;
const root = Array.isArray(obj) ? [] : {};
const stack = [{ target: obj, clone: root }];
const cache = new WeakMap([[obj, root]]);
while (stack.length) {
const { target, clone } = stack.pop();
for (const key of Reflect.ownKeys(target)) {
const value = target[key];
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
clone[key] = cache.get(value);
} else {
const childClone = Array.isArray(value) ? [] : {};
cache.set(value, childClone);
clone[key] = childClone;
stack.push({ target: value, clone: childClone });
}
} else {
clone[key] = value;
}
}
}
return root;
}
// 测试深层嵌套
let deepObj = {};
let current = deepObj;
for (let i = 0; i < 100000; i++) {
current.nested = {};
current = current.nested;
}
// deepCloneRecursive(deepObj); // RangeError: Maximum call stack size exceeded
deepCloneIterative(deepObj); // 正常工作
高频面试题解析
Q1:JSON.parse(JSON.stringify())有什么问题?
答案: | 类型 | 结果 | |——|——| | undefined | 丢失 | | Symbol | 丢失 | | Function | 丢失 | | BigInt | 报错 | | Date | 变成字符串 | | RegExp | 变成空对象 | | Map/Set | 变成空对象 | | 循环引用 | 报错 | | NaN/Infinity | 变成null |
Q2:为什么用WeakMap而不是Map?
答案:
- 弱引用:不阻止垃圾回收
- 自动清理:原对象被销毁时,缓存自动释放
- 防止内存泄漏:长时间运行的应用中,Map会累积无用数据
Q3:Symbol能被深拷贝吗?
答案:
1
2
3
4
5
6
7
8
9
// Symbol是唯一的,无法创建相同的Symbol
const sym = Symbol('key');
const clonedSym = Symbol('key');
sym === clonedSym; // false
// 但Symbol作为属性键可以被拷贝
const obj = { [Symbol('key')]: 'value' };
const cloned = deepClone(obj);
// cloned拥有相同的Symbol属性(引用相同的Symbol对象)
Q4:如何处理DOM元素?
答案:
1
2
3
4
5
6
7
8
9
10
function deepClone(value, cache) {
// ...
// DOM元素不应该被拷贝
if (value instanceof Node) {
return value;
}
// ...
}
Q5:手写一个最简深拷贝(面试30秒版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepClone(obj, cache = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
const clone = Array.isArray(obj) ? [] : {};
cache.set(obj, clone);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], cache);
}
}
return clone;
}
总结与扩展
核心要点回顾
- 基本类型直接返回,引用类型递归拷贝
- 循环引用用WeakMap缓存,防止无限递归
- Symbol用Reflect.ownKeys获取,普通for…in无法访问
- 特殊对象特殊处理:Date、RegExp、Map、Set、ArrayBuffer等
- 原型链用Object.create保持
面试速记口诀
1
2
3
4
5
6
7
8
先判类型再判缓存,
基本类型直接返回。
数组对象递归拷,
Map Set特殊处理。
Symbol属性要遍历,
循环引用WeakMap记。
Date RegExp new一个,
原型链要Object.create。
深入学习资源
相关主题
- 浅拷贝方法对比分析
- 深拷贝循环引用的处理
- Immutable不可变数据原理
- Redux中的不可变更新
深拷贝是JavaScript基础能力的试金石。从最简版本到完整版本,逐步攻克类型判断、循环引用、Symbol属性、特殊对象等难点,面试时才能游刃有余。
本文由作者按照 CC BY 4.0 进行授权