深拷贝详解

168 阅读4分钟

JSON

JSON.parse(JSON.stringify(obj))

​ 这种方法可以说是目前最简单直接的深拷贝方法了。

const obj1 = {
    a: {
        b: 'this is b'
    },
    age: 20
}
const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.a.b = 'now, this is new value'
console.log(obj1) // { a: { b: 'this is b' }, age: 20 }
console.log(obj2) // { a: { b: 'now, this is new value' }, age: 20 }

简单粗暴......

虽然简单,但对于JSON.stringify(target)来说,存在着诸多局限:

  1. 如果target有这三种属性的值:UndefinedFunctionSymbol(值或属性),那么:若是target是对象,它们则会被忽略;若是target是数组,它们则会被序列化成null

  2. **拷贝Date类型时,会变成字符串。**因为Date.prototype中定义了toJSON()方法(返回String)将其转为String。(toJSON()的返回值决定着目标什么值被序列化)

  3. 对于**NaN-InfinityInfinity**这三个值序列化的结果为nullnull的值就是null

  4. 对包含循环引用的对象(obj[key] = obj),则会报错

现在开始改进:


const a1 = {
  age: 20,
  b: {
    toJSON() {
      return 'this is toJSON'
    }
  },
  time: new Date(),
  unde: undefined,
  func: () => ({}),
  symb: Symbol(1),
  num1: NaN,
  num2: Infinity,
  num3: -Infinity
}

function isNaN(target) {
  return target !== target
}

function isInfinity(target) {
  return typeof target === 'number' && !Number.isFinite(target)
}

function isDate(target) {
  return target instanceof Date
}

function replacer(key, value) {
  switch(true) {
    case isNaN(value) || isInfinity(value) : // 考虑NaN / Infinity / -Infinity
      return value.toString()
    case value === undefined: // 考虑 undefined
      return 'undefined'
    case value === 'this is toJSON': // 验证toJSON方法是否运行在replacer之前:答案:确实是之前
      console.log('****toJSON run in before replacer****')
      return value
    default:
      return value
  }
}

function reviver(key, value) {
  switch(true) {
    case value === 'NaN':
      return NaN
    case value === 'Infinity':
      return Infinity
    case value === '-Infinity':
      return -Infinity
    case value === 'undefined':
      return undefined
    case isDate(a1[key]):
      return new Date(value)
    default:
      return value
  }
}

const a2 = JSON.parse(JSON.stringify(a1, replacer), reviver)
console.log(a2)

由代码测试可得,对象的toJSON方法会在传递给'replacer'之前执行,所以,不能在replacer中对Date类型进行处理,从而选择了上面这种笨拙的方式

我们粗暴而又笨拙的利用JSON.stringify的第二个参数'replacer'NaNInfinity/-Infinityundefined进行了字符串化,从而解决了JSON.stringifyNaNInfinity/-Infinity的*null化*,以及undefined忽略。利用JSON.parse的第二个参数'reviver',对Date类型进行处理。

这种方式虽然充分利用了JSON.stringify,JSON.parse的参数,但始终不完美。我们需要对对象属性有个清晰的了解,确保其本身不含有'NaN', 'Infinity/-Infinity', 'undefined'这些字符串值。以及后面对Date的处理。同时,也处理不了其他缺陷

2、递归实现

先来个基础版:

function isPureObject(target) {
  return Object.prototype.toString.call(target).slice(8, -1) === 'Object'
}

function deepClone(target) {
  if(!isPureObject(target)) { 
    return target
  }
  const obj = {} 
  for(let key in target) {
    obj[key] = deepClone(target[key])
  }
  return obj
}


基础版的缺陷是:

  • 处理不了**symbol属性**(并非值,值可以处理)。

  • 只能处理普通引用类型,处理不了Array, Date, RegExp, Error, Function这样的引用类型。

  • 循环引用没有解决。

好,接下来就是逐个解决缺陷的时候了~

对数组的处理:

 + const obj = Array.isArray(target) ? [] : {} 
 - const obj = {}

Date, RegExpError的处理:

// 很简单加上这段代码就行了  
if(target instanceof Date) {
  return new Date(target)
}
if(target instanceof RegExp) {
  return new RegExp(target)
}
if(target instanceof Error) {
  return new Error(target)
}

Symbol属性的处理:

+ for(let key of Reflect.ownKeys(target))
- for(let key in target)

对循环引用的处理:

原理是,使用一个数据结构保存保存已经拷贝过的对象,如果对象已经拷贝过,则去这个数据结构里面取出,如果没拷贝过,执行拷贝的同时需要往这个数据结构里面存储。

选什么数据结构来保存拷贝过的对象呢?Map,WeakMap,Set,weakSet,Object怎么选?首先Object肯定不行,因为,我们的原理是保存obj => cloneObj的这种关系,Object的属性只能是StringSymbol。同时也排除了SetWeakSet。对于MapWeakMap来说,貌似都行。为了更精确一点,本文章选用WeakMap

在最终代码中体现出来

扩展

对于不可枚举属性和原型链上的属性:

const alldesc = Object.getOwnPropertyDescriptors(target)
obj = Object.create(Object.getPrototypeOf(target), alldesc)

最终代码:

function isPureObjectOrArray(target) {
  return Object.prototype.toString.call(target).slice(8, -1) === 'Object' || Array.isArray(target)
}

function deepClone(target, hash = new WeakMap()) {
  // 处理Date
  if (target instanceof Date) {
    return new Date(target)
  }
  // 处理RegExp
  if (target instanceof RegExp) {
    return new RegExp(target)
  }
  // 处理Error
  if (target instanceof Error) {
    return new Error(target)
  }
  if(!isPureObjectOrArray(target)) { 
    return target
  }

  if(hash.has(target)) return hash.get(target)

  let obj
  if(Array.isArray(target)) {
    obj = []
  } else {
    const alldesc = Object.getOwnPropertyDescriptors(target)
    obj = Object.create(Object.getPrototypeOf(target), alldesc)
  }
  hash.set(target, obj)
  for (let key of Reflect.ownKeys(target)) {
    obj[key] = deepClone(target[key], hash)
  }
  return obj
}

recursion-2.png