JS 深拷贝

1. 深拷贝

1.1 递归深拷贝

1.1.1 从复制地址到复制引用类型

深拷贝与浅拷贝的区别就是: 当遇见引用类型 A 时

  • 浅拷贝只会拷贝 A 的地址
  • 深拷贝会在堆内存中将 A 复制一遍为 B, 并返回 B 的地址

故而: 深拷贝的第一步就是复制引用类型, 此时需要保证不可枚举属性和 Symbol 属性都能被拷贝

const deepClone = tar => {
  // 如果要复制的目标为基础类型或函数的话, 则直接返回对应的值/地址(因为函数还有可能绑定了相应的 lexical environment 和 this 指向)
  if (!(typeof tar === 'object' && tar != null)) return tar
  const retObj = {}
  Reflect.ownKeys(tar).forEach(key => {
    // 如果 tar 中存在非函数的引用类型值的话, 则会对该引用值进行递归拷贝, 最终实现深拷贝
    retObj[key] = deepClone(tar[key])
  })

  return retObj
}

1.1.2 支持对数组的深拷贝以及保持原有的原型

// 第一个参数指定原型, 第二个参数则是对 tar 的浅拷贝
const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
// 对于 tar 中的引用类型进行深拷贝
Reflect.ownKeys(tar).forEach(key => {
  retObj[key] = deepClone(tar[key])
})

1.1.3 支持 Map, Set, Date, RegExp 的深拷贝

if (tar instanceof Map) {
  const retObj = new tar.constructor()
  tar.forEach((val, key) => {
    retObj.set(key, deepClone(val))
  })
} else if (tar instanceof Set) {
  const retObj = new tar.constructor()
  tar.forEach(val => {
    retObj.add(deepClone(val))
  })
} else if ([Date, RegExp].includes(tar.constructor)) {
  const retObj = new tar.constructor(tar)
} else { // 对象或数组
  // 第一个参数指定原型, 第二个参数则是对 tar 的浅拷贝
  const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
  // 对于 tar 中的引用类型进行深拷贝
  Reflect.ownKeys(tar).forEach(key => {
    retObj[key] = deepClone(tar[key])
  })
}

1.1.4 解决循环引用问题

这里使用 WeakMap 来解决循环引用:

  • 每当要拷贝一个对象之前, 先查看一下该对象是否有被记录
    • 如果存在记录, 则获取记录, 得到对象的拷贝
  • 每当拷贝一个对象之时, 记录下该对象与该对象的拷贝
const deepClone = (tar, map = new WeakMap) => {
  if (!(typeof tar === 'object' && tar != null)) return tar
  if (map.has(tar)) return map.get(tar)

  if (tar instanceof Map) {
    const retObj = new tar.constructor()
    map.set(tar, retObj) // 在调用 deepClone 之前将 tar-retObj 记录到 map 中, 防止循环引用
    tar.forEach((val, key) => {
      retObj.set(key, deepClone(val, map))
    })
    return retObj
  } else if (tar instanceof Set) {
    const retObj = new tar.constructor()
    map.set(tar, retObj)
    tar.forEach(val => {
      retObj.add(deepClone(val, map))
    })
    return retObj
  } else if (tar instanceof Date || tar instanceof RegExp) {
    const retObj = new tar.constructor(tar)
    map.set(tar, retObj)
    return retObj
  } else { // 对象或数组
    const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
    map.set(tar, retObj)
    Reflect.ownKeys(tar).forEach(key => {
      retObj[key] = deepClone(tar[key], map)
    })
    return retObj
  }
}

1.1.5 最终版本

const deepClone = (tar, map = new WeakMap) => {
  if (!(typeof tar === 'object' && tar != null)) return tar
  if (map.has(tar)) return map.get(tar)

  if (tar instanceof Map || tar instanceof Set) {
    const retObj = new tar.constructor()
    map.set(tar, retObj) // 在调用 deepClone 之前将 tar-retObj 记录到 map 中, 防止循环引用

    retObj.add ? 
      tar.forEach(val => retObj.add(deepClone(val, map))) : 
      tar.forEach((val, key) => retObj.set(key, deepClone(val, map)))

  } else if (tar instanceof Date || tar instanceof RegExp) {
    const retObj = new tar.constructor(tar)
    map.set(tar, retObj)
  } else { // 对象或数组
    const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
    map.set(tar, retObj)
    Reflect.ownKeys(tar).forEach(key => {
      retObj[key] = deepClone(tar[key], map)
    })
  }

  return retObj
}

1.2 JSON.stringify 和 JSON.parse 的深拷贝

let newObj = JSON.parse(JSON.stringify(obj))

由于 JSON 只支持以下几种数据类型

  • Number(只支持 10 进制表示)
  • String(必须要用"包裹)
  • Boolean(true/false)
  • Null(null)
  • Object(对象的最后一个键值对后面不能跟 ,)
  • Array

故而, 当使用 JSON.stringifyJSON.parse 进行深拷贝时, 会有以下问题

  • 对于以 Function, Undefined, Symbol 为值的键值对, key 和 value 都无法拷贝
  • 对于以 Date 为值的键值对, 只拷贝其字符串
  • 对于以 Map, Set, RegExp 为值的键值对, 拷贝时会被转为空对象
  • 对于以 BigInt 为值的键值对或者有循环引用的情况时, 拷贝时会报错
  • 无法拷贝原型链, 不可枚举属性

2. 深拷贝的替代方案

在使用深拷贝时, 我们通常是想要: 获得一个对象一些属性修改后的状态, 并且原对象的状态不变

那么, 我们是否可以只对将要修改的键值对及其路径进行一定程度的拷贝, 而其他的键值对则直接复制地址即可. 这样的话, 不需要对大量的对象进行复制, 从而可以减小资源的消耗, 提高效率.

如下图所示: 当我们需要修改 A.C.F 时, 对于 A, C 我们只需要深拷贝关键路径的键值对即可(如 A.C, C.F 即可. 而 A.B, A.D, C.E, E.G, E.H 只需要浅拷贝即可)

下面是该想法的简单实现

const change = (tar, {path, value}) => {
  const key = path[Symbol.iterator]() // 使用迭代器
  
  const traverse = (tar, prop) => {
    if (prop === undefined) return value 
    
    const retObj = Object.create(Reflect.getPrototypeOf(tar), Object.getOwnPropertyDescriptors(tar))
    retObj[prop] = traverse(tar[prop], key.next().value)
    
    return retObj
  }
  
  return traverse(tar, key.next().value)
}
let obj = {b: 1, c: {e: {g: 2, h: 3}, f: 4}, d: 5}
let ret = change(obj, {path: ["c", "f"], value: 233})

posted @ 2022-11-17 22:20  小阁下  阅读(368)  评论(0编辑  收藏  举报