深拷贝与浅拷贝

浅拷贝

什么是浅拷贝?举个例子:

const a = {
  b: 1,
}
const c = a

console.log(c === a) // true

c.b = 2

console.log(a.b) // 2

我们先将 a 赋值给 c,实际上是将 a 的内存地址分享给 c,两个变量引用的实际上是同一个对象

所以当改变 cb 属性时,ab 属性也会跟着改变

由上可得:浅拷贝只复制对象的第一层属性

深拷贝

由于浅拷贝所得到的值的内存地址是一样的,所以要想改变属性值而不影响其他对象,就需要深拷贝

深拷贝可以将对象的属性进行递归复制

function deepCopy(oldObj, newObj) {
  // 判断newObj是否传入,如果没有初始化一个空对象
  newObj = newObj || {}
  // for...in循环遍历oldObj的key
  for (let item in oldObj) {
    // 判断是否为对象
    if (typeof oldObj[item] === 'object') {
      // 判断属性值是否为数组
      if (objObj[item].constructor === Array) {
        newObj[item] = []
      } else {
        // 如果不是数组,那就是对象
        newObj[item] = {}
      }
      // 递归调用deepCopy
      deepCopy(oldObj[item], newObj[item])
    } else {
      newObj[item] = oldObj[item]
    }
  }
  return newObj
}

这种方法比较麻烦,实际项目中可以使用序列化反序列化的方式实现深拷贝:

const deepCopy = (oldObj) => JSON.parse(JSON.stringify(oldObj))

const a = {
  b: 1,
  c: [1, 2, 3],
  d: {
    e: 1,
  },
}

const f = deepCopy(a)
f.b = 2
f.c[0] = 4
f.d.e = 5

console.log(a)
// {
//   b: 1,
//   c: [1, 2, 3],
//   d: {
//     e: 1
//   }
// }

console.log(f)
// {
//   b: 2,
//   c: [4, 2, 3],
//   d: {
//     e: 5
//   }
// }

可以看到这种方法也可以实现深拷贝。

Notice

这种方式实现的深拷贝有如下几个缺点:

  • 如果对象中存在时间对象会被转换成字符串。
  • 如果对象中里有 RegExp、Error 对象,则序列化的结果将只得到空对象。
  • 如果对象中有函数,undefined,则序列化的结果会把函数,undefined丢失。
  • 如果对象中有NaN、Infinity 和 -Infinity,则序列化的结果会变成 null
  • 如果对象中存在循环引用的情况也无法正确实现深拷贝。

Object.assign

Object.assign 是 ES6 规范中的一个新方法。

也可以实现对象的拷贝,但仅限于第一层的深拷贝,第一层之后一律是浅拷贝:

const deepCopy = (oldObj) => Object.assign({}, oldObj)

const a = {
  b: 1,
  c: [1, 2, 3],
  d: {
    e: 1,
  },
}

const f = deepCopy(a)
f.b = 2
f.c[0] = 4
f.d.e = 5

console.log(a)
// {
//   b: 1,
//   c: [4, 2, 3],
//   d: {
//     e: 5
//   }
// }

console.log(f)
// {
//   b: 2,
//   c: [4, 2, 3],
//   d: {
//     e: 5
//   }
// }

使用 Object.assign 方法并不能达到严格意义上的深拷贝,只能深拷贝第一层的属性而已,因此,如果对象有多层嵌套,就不要用 Object.assign 进行深拷贝

对象扩展符

对象扩展符是 ES7 的标准

还是一样的例子,来看看对象扩展符能否实现深拷贝:

const deepCopy = (oldObj) => ({ ...oldObj })

const a = {
  b: 1,
  c: [1, 2, 3],
  d: {
    e: 1,
  },
}

const f = deepCopy(a)
f.b = 2
f.c[0] = 4
f.d.e = 5

console.log(a)
// {
//   b: 1,
//   c: [4, 2, 3],
//   d: {
//     e: 5
//   }
// }

console.log(f)
// {
//   b: 2,
//   c: [4, 2, 3],
//   d: {
//     e: 5
//   }
// }

结果和 Object.assign 一模一样,同样也只是实现了第一层的深拷贝

深拷贝实现

最近实现了一个比较全面的深拷贝代码:

function deepClone(origin, target, hash = new WeakMap()) {
  //origin:要被拷贝的对象
  // 需要完善,克隆的结果和之前保持相同的所属类
  const target = target || {}

  // 处理特殊情况
  if (origin == null) return origin //null 和 undefined 都不用处理
  if (origin instanceof Date) return new Date(origin)
  if (origin instanceof RegExp) return new RegExp(origin)
  if (typeof origin !== 'object') return origin // 普通常量直接返回

  //  防止对象中的循环引用爆栈,把拷贝过的对象直接返还即可
  if (hash.has(origin)) return hash.get(origin)
  hash.set(origin, target) // 制作一个映射表

  // 拿出所有属性,包括可枚举的和不可枚举的,但不能拿到symbol类型
  const props = Object.getOwnPropertyNames(origin)
  props.forEach((prop, index) => {
    if (origin.hasOwnProperty(prop)) {
      if (typeof origin[prop] === 'object') {
        if (Object.prototype.toString.call(origin[prop]) == '[object Array]') {
          //数组
          target[prop] = []
          deepClone(origin[prop], target[prop], hash)
        } else if (Object.prototype.toString.call(origin[prop]) == '[object Object]') {
          //普通对象
          target[prop] = {}

          deepClone(origin[prop], target[prop], hash)
        } else if (origin[prop] instanceof Date) {
          // 处理日期对象
          target[prop] = new Date(origin[prop])
        } else if (origin[prop] instanceof RegExp) {
          // 处理正则对象
          target[prop] = new RegExp(origin[prop])
        } else {
          //null
          target[prop] = null
        }
      } else if (typeof origin[prop] === 'function') {
        const _copyFn = function (fn) {
          const result = new Function('return ' + fn)()
          for (let i in fn) {
            deepClone[(fn[i], result[i], hash)]
          }
          return result
        }
        target[prop] = _copyFn(origin[prop])
      } else {
        //除了object、function,剩下都是直接赋值的原始值
        target[prop] = origin[prop]
      }
    }
  })

  // 单独处理symbol
  const symKeys = Object.getOwnPropertySymbols(origin)
  if (symKeys.length) {
    symKeys.forEach((symKey) => {
      target[symKey] = origin[symKey]
    })
  }
  return target
}

结论

  • JSON.parse(JSON.stringify(oldObj)) 这种方法可以实现严格意义上的深拷贝,但有许多缺陷。
  • Object.assign 和对象扩展符只能对第一层属性深拷贝,更深层的属性只能浅拷贝。