keguigong

JavaScript对象循环引用和深拷贝

  by  keguigong

定义一个对象 aa 含有属性 bcc 属性指向自己,即 a,则构成循环引用(Circular Reference)。

let a = { b: 'hello world!' }
a.c = a

在 Node.js 环境运行,输出提示循环引用 [Circular * 1]

< ref * 1 > { b: "hello world!", c: [Circular * 1] }

在 Chrome 种运行,输出

{b: 'hello world!', c: {…}}
  b: "hello world!"
  c:
    b: "hello world!"
    c:
      b: "hello world!"
      c: {b: 'hello world!', c: {…}}
      [[Prototype]]: Object
    [[Prototype]]: Object
  [[Prototype]]: Object

发现构成循环引用之后,会输出一个层级无限深的对象。如果现在需要对对象 a 进行拷贝,单纯用递归函数进行处理,则会造成内存溢出,因为永远处理不完。所以针对循环引用的场景,在拷贝的时候需要进行单独的处理。

假设拷贝对象 target 只为基础对象类型,使用下面的简易 clone() 函数拷贝对象 a

function clone(target) {
  if (typeof target !== 'object') return target
  let obj = {}
  for (const key in target) {
    obj[key] = clone(target[key])
  }
  return obj
}

则会产生溢出错误 Uncaught RangeError: Maximum call stack size exceeded

Map(了解 Map 类型)的 key 可以存储对象,且不能相同,可以用来存储一下每次拷贝的值,考虑到回收的问题,我们最终采用 WeakMap 来保存每次拷贝的值,下次深拷贝前,先判断下是否已有相同对象存在,若有,直接返回对应的对象即可,解决循环引用的问题,不再一层一层递归下去。

function clone(target) {
  const map = new WeakMap()
  function _clone(target) {
    if (typeof target !== 'object') return target
    if (map.get(target)) return target
    let res = {}
    map.set(target, res)
    for (let key in target) {
      res[key] = _clone(target[key])
    }
    return res
  }
  return _clone(target)
}

使用该方法就可以解决循环引用的拷贝问题,同时也可以解决 Symbol 类型的拷贝。其他类型的拷贝就正常处理即可,完整的函数如下

function clone(target) {
  const map = new WeakMap()
 
  function _clone(target) {
    if (typeof target !== 'object') return target
    if (map.get(target)) return target
    let res
    if (target instanceof Function) {
      if (target.prototype)
        res = function () {
          return target.apply(this, arguments)
        }
      else res = (...args) => target.apply(undefined, args)
    }
    // Other objects
    else if (target instanceof Array) res = []
    else if (target instanceof Date) res = new Date(target - 0)
    else if (target instanceof RegExp) res = new RegExp(target.source, target.flags)
    else res = {}
    map.set(target, true)
 
    for (let key in target) {
      if (target.hasOwnProperty(key)) {
        res[key] = _clone(target[key])
      }
    }
    return res
  }
  return _clone.apply(this, target)
}

References: