文章

手写完整深拷贝函数深度解析

从零手写支持多种数据类型、循环引用、Symbol的完整深拷贝函数,深入理解引用类型复制原理

手写完整深拷贝函数深度解析

一句话概括

深拷贝是递归复制所有层级属性的技术,核心难点在于处理循环引用、特殊对象类型和Symbol属性,是面试手写题的必考项。

背景

在前端面试中,”手写深拷贝”是最高频的手写题之一,原因有三:

  1. 考察全面性:涉及类型判断、递归、数据结构、边界处理
  2. 实战价值高:工作中经常需要复制复杂对象
  3. 区分度高:能区分初级和中级前端

常见面试追问:

  • “怎么处理循环引用?”
  • “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方法的局限

  • 无法处理undefinedSymbolBigIntFunction
  • Date变成字符串,RegExp变成空对象
  • MapSetError等特殊对象丢失
  • 无法处理循环引用(报错)
  • 丢失原型链

最小示例

基础版深拷贝

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...inObject.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?

答案

  1. 弱引用:不阻止垃圾回收
  2. 自动清理:原对象被销毁时,缓存自动释放
  3. 防止内存泄漏:长时间运行的应用中,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;
}

总结与扩展

核心要点回顾

  1. 基本类型直接返回,引用类型递归拷贝
  2. 循环引用用WeakMap缓存,防止无限递归
  3. Symbol用Reflect.ownKeys获取,普通for…in无法访问
  4. 特殊对象特殊处理:Date、RegExp、Map、Set、ArrayBuffer等
  5. 原型链用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 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量