简单版

1. 利用JSON.parse(JSON.stringify(obj))

var obj = {a: 1, b: [2], c: {d: 4}};
var copyObj = JSON.parse(JSON.stringify(obj));

此种方法不能拷贝undefined, Function、Symbol、RegExp、Date、Set、Map等,以及不能解决循环引用;

2 简易版深拷贝(是对象就递归调用)

先来判断对象

// 判断是否是对象
function isObject(target) {
  const type = typeof target;
  return target !== null && type === "object";
}
function deepClone(target) {
  if (isObject(target)) {
    let cloneTarget = target instanceof Array ? [] : {};
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = deepClone(target[key]);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

但这种方式没有解决循环引用的问题:

var obj = {a: 1, b: [2], c: {name: 'zxx'}};
obj.obj = obj;
deepClone(obj);

执行结果如下: 

js 深度拷贝对象 JavaScript基于遍历操作实现对象深拷贝功能示例 js深拷贝的实现方式_JSON

很明显,因为递归进入死循环导致栈内存溢出了。

手写一个深拷贝

解决循环引用问题

解决循环引用问题,我们可以利用额外的存储空间。当需要拷贝当前对象时,先去存储空间中去找,没有的话就继续拷贝,有的话就直接返回,这样就可以解决循环引用的问题。

这里我们使用WeakMap这种数据结构(WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。可以解决垃圾回收问题)

function deepClone(target, map = new WeakMap()) {
  if (isObject(target)) {
    let cloneTarget = Array.isArray(target) ? [] : {};
    // 防止循环引用
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);
    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = deepClone(target[key], map);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

测试:

var obj = {a: 1, b: [2], c: {name: 'zxx'}};
obj.obj = obj;
deepClone(obj);

js 深度拷贝对象 JavaScript基于遍历操作实现对象深拷贝功能示例 js深拷贝的实现方式_循环引用_02

但上面只考虑了Array,Object两种数据类型,实际上所有的引用类型远远不止这两个。 

其他数据类型

获取数据类型

下面方法就是获取数据类型:

因为大部分引用类型比如Array、Date、RegExp等都重写了toString方法,所以我们直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果

function getType(target) {
    return Object.prototype.toString.call(target);
}

调用

结果

Object.prototype.toString.call(1)

[object Number]

Object.prototype.toString.call("hello")

[object String]

Object.prototype.toString.call(true)

[object Boolean]

Object.prototype.toString.call(null)

[object Null]

Object.prototype.toString.call(undefined)

[object Undefined]

Object.prototype.toString.call(Symbol())

[object Symbol]

Object.prototype.toString.call({})

[object Object]

Object.prototype.toString.call([])

[object Array]

Object.prototype.toString.call(function() {})

[object Function]

Object.prototype.toString.call(new Error())

[object Error]

Object.prototype.toString.call(new Date())

[object Date]

Object.prototype.toString.call(new RegExp())

[object RegExp]

Object.prototype.toString.call(Math)

[object Math]

Object.prototype.toString.call(JSON)

[object JSON]

Object.prototype.toString.call(document)

[object HTMLDocument]

Object.prototype.toString.call(window)

[object Window]

 还有Arguments、Set、Map、WeakSet、WeakMap等。。。

 根据上面类型是否可遍历可以分为继续遍历的类型、不可以继续遍历的类型

 处理可遍历类型

上面已经处理了Object、Array,另外还有MapSet、Arguments等都是可以继续遍历的类型

先获取它们的初始化数据

function getInit(target) {
    return new target.constructor();
}

 处理可遍历类型

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';


const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
    cloneTarget = getInit(target, type);
}

 处理Set、Map

// 克隆set
if (type === setTag) {
  target.forEach((value) => {
    cloneTarget.add(deepClone(value, map));
  });
  return cloneTarget;
}

// 克隆map
if (type === mapTag) {
  target.forEach((value, key) => {
    cloneTarget.set(key, deepClone(value, map));
  });
  return cloneTarget;
}

处理不可遍历类型

BoolNumberStringStringDateError这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

克隆Symbol类型:

function cloneSymbol(target) {
  return Object(Symbol.prototype.valueOf.call(target));
}

克隆正则

function cloneReg(target) {
  const reFlags = /\w*$/;
  const result = new target.constructor(target.source, reFlags.exec(target));
  result.lastIndex = target.lastIndex;
  return result;
}
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';



function cloneOtherType(target, type) {
  const Ctor = target.constructor;
  switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return cloneReg(target);
    case symbolTag:
      return cloneSymbol(target);
    default:
      return null;
  }
}

拷贝函数

函数需要区分下箭头函数和普通函数,我们可以通过prototype来区分,箭头函数是没有prototype的。

function cloneFunction(func) {
    let funcString = func.toString();
    if (func.prototype) {
        return new Function('return ' + funcString)();
    } else {
        return eval(funcString);
    }
}

最后,完整的代码:

const boolTag = "[object Boolean]";
const dateTag = "[object Date]";
const errorTag = "[object Error]";
const numberTag = "[object Number]";
const regexpTag = "[object RegExp]";
const stringTag = "[object String]";
const symbolTag = "[object Symbol]";
const functionTag = "[object Function]";

const mapTag = "[object Map]";
const setTag = "[object Set]";
const arrayTag = "[object Array]";
const objectTag = "[object Object]";
const argsTag = "[object Arguments]";

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];

// 判断是否是对象
function isObject(target) {
  const type = typeof target;
  return target !== null && (type === "object" || type === "function");
}

// 获取它们的初始化数据
function getInit(target) {
    return new target.constructor();
}

// 获取实际类型
function getType(target) {
  return Object.prototype.toString.call(target);
}

// 拷贝Symbol
function cloneSymbol(target) {
  return Object(Symbol.prototype.valueOf.call(target));
}

// 拷贝RegExp
function cloneReg(target) {
  const reFlags = /\w*$/;
  const result = new target.constructor(target.source, reFlags.exec(target));
  result.lastIndex = target.lastIndex;
  return result;
}

// 拷贝function
function cloneFunction(func) {
  let funcString = func.toString();
  // 区分箭头函数和普通函数
  if (func.prototype) {
    return new Function("return " + funcString)();
  } else {
    return eval(funcString);
  }
}

// 拷贝其他数据类型(不可遍历类型)
function cloneOtherType(target, type) {
  const Ctor = target.constructor;
  switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return cloneReg(target);
    case symbolTag:
      return cloneSymbol(target);
    case functionTag:
      return cloneFunction(target);
    default:
      return null;
  }
}

function deepClone(target, map = new WeakMap()) {
  // 原始类型直接返回
  if (!isObject(target)) {
    return target;
  }

  // 初始化
  const type = getType(target);
  let cloneTarget;
  // 是否可遍历
  if (deepTag.includes(type)) {
    cloneTarget = getInit(target, type);
  } else {
    return cloneOtherType(target, type);
  }

  // 防止循环引用
  if (map.get(target)) {
    return map.get(target);
  }
  map.set(target, cloneTarget);

  // set
  if (type === setTag) {
    target.forEach((value) => {
      cloneTarget.add(deepClone(value, map));
    });
    return cloneTarget;
  }

  // map
  if (type === mapTag) {
    target.forEach((value, key) => {
      cloneTarget.set(key, deepClone(value, map));
    });
    return cloneTarget;
  }

  // 克隆对象和数组
  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      cloneTarget[key] = deepClone(target[key], map);
    }
  }
  return cloneTarget;
}

测试:

var map = new Map();
map.set("name", "zxx");
var set = new Set();
set.add("xxx");

var obj = {
  num: 1,
  str: "hello",
  bool: true,
  symbol: Symbol("test"),
  arr: [1, 2, 3],
  o: { name: "zxx" },
  map: map,
  set: set,
  regExp: /\d+/,
  date: new Date(),
  arrowFn: () => {
    console.log("arrowFn");
  },
  fn: function (a, b, c) {
    return a + b - c;
  },
  error: new Error("error"),
};
obj.obj = obj;
deepClone(obj);

输出结果:

js 深度拷贝对象 JavaScript基于遍历操作实现对象深拷贝功能示例 js深拷贝的实现方式_javascript   深拷贝_03