JS 对象浅拷贝与深拷贝

文章整理自我的个人博客文章:每日 Web 前端面试题(2021.05)

浅拷贝

浅拷贝后的对象的属性如果是引用类型,它仍旧指向源对象的同名属性。修改浅拷贝对象的引用类型下的属性,源对象也会被修改。实现方案有:

  • {...obj}
  • Object.assign({}, obj)
  • for...in 遍历对象获取 key,放到一个新的空对象里。

深拷贝

拷贝出来的对象,修改引用类型属性下的属性,源对象也不会变。二者真正意义上的分道扬镳。实现方案有 序列化并反序列化,以及 递归实现

序列化并反序列化

JSON.parse(JSON.stringify())。缺点是:

  • 复制特殊的对象会出现问题,如 Date、RegExp
  • 不能处理有环的对象,会报错
  • 如果两个属性指向同一个对象,新对象的这两个同名属性指向各自的属性

手动实现

核心思路是:遍历属性,对于引用类型的属性,对其 递归,递归到所有属性都不是引用类型为止。

需要解决的问题:

  • 循环引用,即对象下的某个属性指向这个对象。如果不进行特殊处理地递归,将会死循环。处理环的问题,通常是使用哈希表的方式解决:将遍历到的对象放到哈希表里(key 为源对象,value 为新对象),拷贝前检查哈希表是否有这个对象,有说明出现环,直接取对应的 value 为新对象即可。
  • 相同引用,即多个属性指向同一个对象。循环引用是相同引用的特例。其实解决方案就是 循环引用 的处理方案。
  • 特殊对象,普通的对象、数组这两个最基本的不必说,JS 内建的对象如 Date、RegExp 都要一个个做针对性的处理。如果还要考虑用户自定义的对象,也要针对性地处理,如补上原型链,针对性地实现 clone 方法等。

实现代码如下(对各种情况都进行了简单的单元测试):

/**
 * 对象深拷贝
 * 目前仅支持 普通对象 和 数组
 * 处理了 循环引用 和 相同引用 的问题
 * 
 * @param {any} obj 被拷贝对象
 * @returns 新对象
 */
const cloneDeep = (obj) => {
  const map = new Map() // oldObj -> newObj

  function _cloneDeep(obj) {
    if (obj === null || typeof obj !== 'object') return obj // 非对象值
    if (map.has(obj)) { // 检测相同引用
      return map.get(obj)
    }

    const retObj = Array.isArray(obj) ? [] : {} // (确定新对象的载体,是普通对象还是数组)
    // 【易错点】这句代码必须放这里,不能放在 return 的前面,这是为了处理 “循环引用” 的情况
    // 需要在发现循环对象时,这个循环对象已经在 map 中,否则会死循环。
    map.set(obj, retObj)
    for (const key in obj) { // 遍历属性
      retObj[key] = _cloneDeep(obj[key])
    }
    return retObj
  }

  return _cloneDeep(obj)
}
复制代码

实际开发中并不需要过于完善的深拷贝,一般序列化反序列化就足够了。如果情况有些复杂,可以考虑使用 loadsh.cloneDeep

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享