目录

手动实现一个深拷贝

1、 JSON.parse()JSON.stringify()

最简单的方式是使用 JSON.parse()JSON.stringify() 进行深拷贝:

1
2
3
function cloneDeep(target) {
  return JSON.parse(JSON.stringify(target));
}

但是它有一些缺陷:

  • 无法处理对象内的循环引用
  • 无法保持之前的原型链
  • 无法处理 bigint 类型
  • 无法正确处理 正则Date 类型
  • 会忽略 functionundefined 类型
  • 会忽略 key 为 symbol 类型的字段

2、MessageChannel

还可以使用 MessageChannel 来进行深拷贝:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function deepClone(target) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port2.onmessage = (ev) => resolve(ev.data);
    port1.postMessage(target);
  });
}

const obj = {
  a: 1,
  b: {
    c: 1,
  },
};
// 注意该方法是异步的
const cloneObj = await deepClone(obj);

上面的方法可以正确处理对象内的循环引用、bigint正则Dateundefined 类型;

但是下面的问题依然没有解决:

  • 无法保持之前的原型链
  • 会忽略 key 为 symbol 类型的字段

而且如果对象中有 function,会直接报错。异步的方式用起来相对麻烦。

3、手动实现一个深拷贝方法

(1)、简单版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function cloneDeep(target) {
  if (target === null) return null;
  if (typeof target !== "object") return target;
  const newTarget = Array.isArray(target) ? [] : {};
  for (let key in target) {
    //不遍历其原型链上的属性
    if (target.hasOwnProperty(key)) {
      newTarget[key] = cloneDeep(target[key]);
    }
  }
  return newTarget;
}

(2)、正确处理 正则Date 类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  function cloneDeep(target) {
    if (target === null) return null;
    if (typeof target !== 'object') return target;
+   if (target.constructor === Date) return new Date(target);
+   if (target.constructor === RegExp) return new RegExp(target);
    const newTarget = Array.isArray(target) ? [] : {};
    for (let key in target) {
      //不遍历其原型链上的属性
      if (target.hasOwnProperty(key)) {
        newTarget[key] = cloneDeep(target[key]);
      }
    }
    return newTarget;
  };

(3)、保持之前的原型链

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  function cloneDeep(target) {
    if (target === null) return null;
    if (typeof target !== 'object') return target;
    if (target.constructor === Date) return new Date(target);
    if (target.constructor === RegExp) return new RegExp(target);
+   const newTarget = new target.constructor();     // 保持原型链
-   const newTarget = Array.isArray(target) ? [] : {};
    for (let key in target) {
      //不遍历其原型链上的属性
      if (target.hasOwnProperty(key)) {
        newTarget[key] = cloneDeep(target[key]);
      }
    }
    return newTarget;
  };

(4)、正确处理 key 为 symbol 类型的字段

for .. in .. 遍历不到 Symbol 类型的 key,其他的方式一般也遍历不到 Symbol 类型的 key ;

这里可以借助 Reflect.ownKeys 方法,他可以遍历当前对象的所有 key:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  function cloneDeep(target) {
    if (target === null) return null;
    if (typeof target !== 'object') return target;
    if (target.constructor === Date) return new Date(target);
    if (target.constructor === RegExp) return new RegExp(target);
    const newTarget = new target.constructor();     // 保持原型链
-   for (let key in target) {
-     //不遍历其原型链上的属性
-     if (target.hasOwnProperty(key)) {
-       newTarget[key] = cloneDeep(target[key]);
-     }
-   }
+   Reflect.ownKeys(target).forEach(key => {
+     newTarget[key] = cloneDeep(target[key]);
+   })
    return newTarget;
  };

(5)、处理对象内的循环引用

处理循环引用的思路很简单,设置一个哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回,不再做后续操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
+ function cloneDeep(target, map = new Map()) {
- function cloneDeep(target) {
    if (target === null) return null;
    if (typeof target !== 'object') return target;
    if (target.constructor === Date) return new Date(target);
    if (target.constructor === RegExp) return new RegExp(target);
+   if (map.has(target)) return map.get(target);
    const newTarget = new target.constructor();
+   map.set(target, newTarget);
    Reflect.ownKeys(target).forEach(key => {
+     newTarget[key] = cloneDeep(target[key], map);
-     newTarget[key] = cloneDeep(target[key]);
    })
    return newTarget;
  };

最终的深拷贝代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function cloneDeep(target, map = new Map()) {
  if (target === null) return null;
  if (typeof target !== "object") return target;
  if (target.constructor === Date) return new Date(target);
  if (target.constructor === RegExp) return new RegExp(target);
  if (map.has(target)) return map.get(target);
  const newTarget = new target.constructor();
  map.set(target, newTarget);
  Reflect.ownKeys(target).forEach((key) => {
    newTarget[key] = cloneDeep(target[key], map);
  });
  return newTarget;
}

它可以:

  • 正确处理对象内的循环引用
  • 保持之前的原型链
  • 正确处理 bigint 类型
  • 正确处理 正则Date 类型
  • 正确处理 functionundefined 类型
  • 正确处理 key 为 symbol 类型的字段

参考链接:

如何实现一个深拷贝

ECMAScript 6 入门:Reflect