深入分析 —— JavaScript 深拷贝

前言

本文将逐步实现一个js深拷贝方法,实现字段描述、Symbol、非枚举字段、原型、循环引用等各种场景值的深拷贝。

封装方法

js 复制代码
// 拷贝方法
function deepClone(source) {
  function loop(source) {
    // todo
  }
  return loop(source)
}

// 验证方法 用于打印和验证结果
function compare(obj1, obj2) { 
  let consoleText = ''
  function loop(obj1, obj2, times = 0, prevKey = '') { 
    if (times > 4) return
    Object.keys(obj1).forEach(key => {
      const currentKey = prevKey ? `${prevKey}.${key}` : key
      consoleText += `${currentKey} is ${obj1[key] === obj2[key] ? '' : 'not '}equal \n`
      console.log(`${currentKey} > `, obj1[key], obj2[key])
      if (typeof obj1[key] === "object" || typeof obj2[key] === "object") {
        loop(obj1[key], obj2[key], times + 1, currentKey)
      }
    })
  }
  loop(obj1, obj2)
  console.log(consoleText)
}

基本拷贝

js 复制代码
const demo = {
  string: 'string',
  number: 0,
  boolean: true,
  object: { key: 'key' },
  array: [1, 2],
}

实现迭代拷贝字符串,数字,布尔值,基本对象,基本数组:

通过循环key,判断如果是对象,则递归,否则赋值,这里不多做解释,

js 复制代码
function deepClone(source) {
  // 递归方法
  function loop(source) {
    if (typeof source !== "object") return new Error('source must be an object!')
    
    // 判断复制的目标是数组还是对象
    const targetObj = source.constructor === Array ? [] : {}
    // 遍历 key 如果依然是对象,则继续迭代,否则赋值
    Object.keys(source).forEach(key => {
      let newValue
      if (typeof source[key] === "object") {
        newValue = loop(source[key])
      } else { 
        newValue = source[key]
      }
      targetObj[key] = newValue
    })
    return targetObj
  }
  return loop(source)
}

const cloneDemo = deepClone(demo)
compare(demo, cloneDemo)

日期,正则等类型拷贝

js 复制代码
const demo = {
  date: new Date(),
  reg: /^\d+$/,
}

上述方法拷贝以上数据结果却是空对象,与预期不符,所以需要针对这类类型进行特殊处理:

通过判断继承类型,按规则重新初始化数据。

js 复制代码
function deepClone(source) {
  function loop(source) {
    。。。
    Object.keys(source).forEach(key => {
      let newValue
      if (typeof source[key] === "object") {
        // Date
        if (source[key] instanceof Date) newValue = new Date(source[key])
        // RegExp
        else if (source[key] instanceof RegExp) newValue = new RegExp(source[key])
        // todo 按需添加其他判断
        else newValue = loop(source[key])
      } else { 
        newValue = source[key]
      }
      targetObj[key] = newValue
    })
    return targetObj
  }
  return loop(source)
}

原型链拷贝

js 复制代码
class Test { }
class Test1 extends Test { 
  action() { }
}
const demo = {
  test: new Test(),
  test1: new Test1(),
}

上述方法拷贝以上数据会丢弃对象原型,与预期不符:

将拷贝值原型指向被拷贝值原型

js 复制代码
function deepClone(source) {
  function loop(source) {
    。。。
    // 判断复制的目标是数组还是对象
    const targetObj = source.constructor === Array ? [] : {}
    // 原型指向目标原型
    Object.setPrototypeOf(targetObj, Object.getPrototypeOf(source))
    。。。
  }
  return loop(source)
}

SymbolKey,不可枚举键,或包含其它属性描述的键的拷贝

js 复制代码
const symbolKey = Symbol()
const demo = {
  [symbolKey]: 1,
}
Object.defineProperty(demo, "specialKey", {
  value: "B",
  enumerable: false, // 是否可枚举
  configurable: false, // 是否可删除/改变
  writable: false, // 是否可赋值
})

上述方法拷贝以上数据会丢弃属性描述,对于不可枚举键更是直接丢弃。与预期不符:

读取属性描述,赋值修改使用值属性描述赋值,遍历方法修改为可遍历SymbolKey的Reflect.ownKeys

js 复制代码
function deepClone(source) {
  
  // 递归方法
  function loop(source) {
    ...
    // 获取原对象属性描述
    let sourceDec = Object.getOwnPropertyDescriptors(source)
    - Object.keys(source).forEach(key => {
    + Reflect.ownKeys(sourceDec).forEach(key => {
      ...
      - targetObj[key] = newValue
      + Object.defineProperty(targetObj, key, { ...sourceDec[key], value: newValue })
    })
    return targetObj
  }
  return loop(source)
}

循环引用对象的拷贝

js 复制代码
const demo = {}
demo.b = demo

上述方法拷贝以上数据会直接导致死循环。与预期不符:

缓存拷贝过的对象,如果要拷贝的对象是已经拷贝过的,则直接返回上次拷贝的结果

js 复制代码
function deepClone0(source) {
  // 拷贝过的映射
  let map = new WeakMap()
  
  function loop(source) {
    ...
    if (map.has(source)) {
      return map.get(source)
    } else {
      // 存储拷贝映射
      map.set(source, targetObj)
      // 原型指向目标原型
      Object.setPrototypeOf(targetObj, Object.getPrototypeOf(source))
      ...
    }
    return targetObj
  }
  return loop(source)
}

完整代码

js 复制代码
function deepClone0(source) {
  // 拷贝过的映射
  let map = new WeakMap()
  // 递归方法
  function loop(source) {
    // 判断复制的目标是数组还是对象
    const targetObj = source.constructor === Array ? [] : {}
    // 如果对象是拷贝过的,直接指向上次拷贝的结果
    if (map.has(source)) {
      return map.get(source)
    } else {
      // 存储拷贝映射
      map.set(source, targetObj)
      // 原型指向目标原型
      Object.setPrototypeOf(targetObj, Object.getPrototypeOf(source))
      // 获取原对象属性描述
      let sourceDec = Object.getOwnPropertyDescriptors(source)
      // 遍历目标 Reflect.ownKeys可以遍历Symbol key 及不可枚举 key
      Reflect.ownKeys(sourceDec).forEach(key => {
        let newValue
        if (source[key] && typeof source[key] === "object") {
          // Date
          if (source[key] instanceof Date) newValue = new Date(source[key])
          // RegExp
          else if (source[key] instanceof RegExp) newValue = new RegExp(source[key])
          // Number
          else if (source[key] instanceof Number) newValue = Number(source[key])
          // 按需添加其他判断
          // other Object 如果值是对象,就递归一下
          else newValue = loop(source[key])
        } else newValue = source[key]
        Object.defineProperty(targetObj, key, {
          ...sourceDec[key],
          value: newValue,
        })
      })
    }
    return targetObj
  }
  return loop(source)
}

实际应用中,大部分上述拷贝场景并不会涉及到,这里仅做说明

相关推荐
J***Q2922 小时前
Vue数据可视化
前端·vue.js·信息可视化
ttod_qzstudio3 小时前
深入理解 Vue 3 的 h 函数:构建动态 UI 的利器
前端·vue.js
芳草萋萋鹦鹉洲哦4 小时前
【elemen/js】阻塞UI线程导致的开关卡顿如何优化
开发语言·javascript·ui
_大龄4 小时前
前端解析excel
前端·excel
1***s6324 小时前
Vue图像处理开发
javascript·vue.js·ecmascript
槁***耿4 小时前
JavaScript在Node.js中的事件发射器
开发语言·javascript·node.js
一叶茶4 小时前
移动端平板打开的三种模式。
前端·javascript
前端大卫4 小时前
一文搞懂 Webpack 分包:async、initial 与 all 的区别【附源码】
前端
U***49834 小时前
JavaScript在Node.js中的Strapi
开发语言·javascript·node.js
Want5955 小时前
HTML音乐圣诞树
前端·html