如何实现一个让面试官惊艳的深克隆

3,078 阅读4分钟

深克隆要解决的问题:正确性,完整性,独立性

  • 正确性: 值不能变
  • 完整性: 不能缺少属性,比如不可枚举属性,symbol, 原型链
  • 独立性:不能影响被克隆对象, 区分值的复制还是引用的复制

一共八种数据类型:
七种基本数据类型: String、Number、Boolean、Null、Undefined、Symbol、BigInt
还有一个万恶的Object: Array, Map,Set, WeakMap, WeakSet, Date,和几乎所有通过 new keyword 创建的东西。

浅克隆

展开运算符...

只能扩展和深拷贝第一层的值

const a = [[1], [2], [3]];
const b = [...a];
b.shift().shift(); 
a // [[], [2], [3]]
b // [[2], [3]]

Object.assign

拷贝源对象自身的并且可枚举的属性

const obj = {};
Object.defineProperty(obj, 'x', { enumerable: false, value: 15 });
obj.test = {
  name: '啊'
}
const cloneObj = {};
Object.assign(cloneObj, obj);
cloneObj.test.name = '啊啊'
console.log('obj', obj) // {test: {name: '啊啊'}, x: 15}
console.log('cloneObj', cloneObj) // {test: {name: '啊啊'}}

还有数组的一些方法,都是浅克隆,Array.prototype.concat, Array.prototype.slice等
我们可以看出来浅克隆主要是独立性达不到,修改克隆后的对象会影响原始数据,

深克隆

1. JSON.parse和JSON.stringify

const person = Object.create(
  null,
  {
    x: { value: 'x', enumerable: false },
    y: { value: 'y', enumerable: true }
  }
)
const symbolName = Symbol(2)
let user = {
  name: "松",
  age: 22,
  firends: [
    {
      name: "小黄",
      age: 22,
    }
  ],
  time: new Date(),
  error: new Error('error'),
  regExp: new RegExp(),
  func: function () { },
  defined: undefined,
  symbol: Symbol(1),
  [symbolName]: 1,
  nan: NaN,
  infinity: Infinity,
  person: person
}
const copyUser = JSON.parse(JSON.stringify(user))
console.log('copyUser', copyUser)
// 输出
{
  name: '松',
  age: 22,
  firends: [
    {
      name: "小黄",
      age: 22,
    }
  ],
  time: '2021-12-22T06:18:44.171Z',
  error: {},
  regExp: {},
  nan: null,
  infinity: null,
  person: { y: 'y'},
}

优点:

简单, 方便

缺点:

  1. 值为Function,Symbol,Undefined,key为Symbol,序列化后会丢失;
  2. 对象存在循环引用会报错
  3. Date对象, 序列化后会变为字符串;
  4. RegExp、Error对象,序列化后得到空对象;
  5. NaN、Infinity,-Infinity,序列化的结果会变成null
  6. 序列化只会处理对象的可枚举的自有属性

2. 递归循环

这是网上传播最多的版本,其实有很多问题

function deepClone(obj) {
  let newObj = Array.isArray(obj) ? [] : {}
  if (obj && typeof obj === "object") {
      for (let key in obj) {
          if (obj.hasOwnProperty(key)) {
              newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key];
          }
      }
  } 
  return newObj
}

优点

独立性: 通过递归,深度复制每个属性值,
正确性: 直接赋值,没有经过转化

缺点

  1. 完整性:其它对象类型都没有考虑,如Date, RegExp等
  2. 如果一个对象的隐式原型为undefined,比如上方通过Object.create可以创建的隐式原型为空的对象,不存在hasOwnProperty这个方法,会直接报错。
  3. 没有考虑递归爆栈
  4. 没有考虑循环引用

终极版

注意现在eval和Function不能直接在浏览器中运行,不然会报错unsafe-eval

function funcClone(func) {
  let paramReg = /\((.*?)\)/
  let bodyReg = /\{(.*)\}/g
  let funcString = func.toString()
  if (func.prototype) {
    let param = paramReg.exec(funcString)
    let body = bodyReg.exec(funcString)
    if (body) {
      if (param) {
        let arrParam = param[1].split(',')
        return new Function(...arrParam, body[1])
      } else {
        return new Function(body[1])
      }
    }
  } else {
    return eval(funcString)
  }
}

const deepcloneTest = function (obj, hash = new WeakMap()) {
  const root = {}
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: obj,
    }
  ];

  while (loopList.length) {
    const { parent, data, key } = loopList.pop()

    let res = parent;
    if (typeof key !== 'undefined') {
      res = parent[key] = {};
    }

    if (hash.has(data)) {
      parent[key] = hash.get(data)
      continue
    }

    const type = [Date, RegExp, Set, Map]
    if (type.includes(data.constructor)) {
      parent[key] = new data.constructor(data)
      continue
    }

    const allDesc = Object.getOwnPropertyDescriptors(data)
    Object.defineProperties(res, allDesc)
    res.__proto__ = Object.getPrototypeOf(data)

    hash.set(data, res)

    for (let k of Reflect.ownKeys(data)) {
      if (data[k] !== null && typeof data[k] === 'object') {
        loopList.push({
          parent: res,
          key: k,
          data: data[k],
        })
      } else if (typeof data[k] === 'function') {
        res[k] = funcClone(data[k])
      } else {
        res[k] = data[k]
      }
    }
  }

  return root
}

function createData(deep, breadth = 0) {
  var data = {};
  var temp = data;

  for (var i = 0; i < deep; i++) {
    temp = temp['data'] = {};
    for (var j = 0; j < breadth; j++) {
      temp[j] = j;
    }
  }

  return data;
}

function Person() {
  console.log("person")
}
let myPerson = new Person()

const enumer = Object.create(
  null,
  {
    x: { value: 'x', enumerable: false, writable: false },
    y: { value: 'y', enumerable: true }
  }
)

let obj = {
  name: '啊啊',
  sex: 1,
  boolean: true,
  array: [{
    apple: 1,
  }, 2, 3],
  null: null,
  undefined: undefined,
  Symbol: Symbol(2),
  bigint: BigInt(100),
  func: function () { console.log("func") },
  arrow: () => { console.log("arrow") },
  date: new Date(),
  regExp: new RegExp(),
  person: myPerson,
  enumer,
}
obj.loop = obj

deepcloneTest(createData(100000)) // 检查是否爆栈
let newObj = deepcloneTest(obj)

console.log('result', newObj)
console.log('检验方法复制', newObj.func === obj.func)
console.log('检验普通对象', newObj.array === obj.array)
console.log('检验原型', newObj.person.constructor)
console.log(newObj.enumer.y)
newObj.enumer.y = 2
console.log('检验描述对象属性', newObj.enumer.y) // 无法修改, 严格模式下会报错,
console.log('检验循环属性', newObj.loop === obj.loop)
  • Object.defineProperties(res, allDesc) 可以复制属性的描述对象
  • Object.getPrototypeOf(data) 可以获取原型链
  • Reflect.ownKeys 可获取不可枚举和symbol属性
  • 各类对象可以使用new keyword实现
  • weakMap解决循环引用的问题
  • 使用循环解决递归爆栈的问题

WeakSet, WeakMap主要用途是避免内存泄漏,复制没有意义,其次是无法枚举,不知道key,无法拿到value

总结要点:

  1. 要考虑各种数据类型,普通类型和对象类型,对象又分为普通对象,方法,和其它使用new keyword实现的对象
  2. 考虑属性的描述对象和原型链
  3. 方法复制比较特殊,
  4. 要考虑循环引用
  5. 考虑递归爆栈

参考文章:
segmentfault.com/a/119000001…
blog.csdn.net/lyt_angular…