深入分析 —— 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)
}

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

相关推荐
让时光到此为止。34 分钟前
vue的首屏优化是怎么做的
前端·javascript·vue.js
San301 小时前
JavaScript 流程控制与数组操作全解析:从条件判断到数据高效处理
javascript·面试·代码规范
温宇飞1 小时前
CSS 中如何处理空白字符
前端
dengzhenyue1 小时前
矩形碰撞检测
开发语言·前端·javascript
llq_3501 小时前
为什么 npm view yarn version 显示 1.22.22?
前端
aesthetician1 小时前
ReactFlow:构建交互式节点流程图的完全指南
前端·流程图·react
neo_dowithless1 小时前
多语言维护太痛苦?我自研了一个翻译自动化 CLI 工具
前端·ai编程
小徐_23331 小时前
老乡鸡也开源?我用 Trae SOLO 做了个像老乡鸡那样做饭小程序!
前端·trae
前端伪大叔2 小时前
第13篇:🎯 如何精准控制买入卖出价格?entry/exit\_pricing 实战配置
javascript·python
荒诞英雄2 小时前
菠萝滞销,帮帮我们(多个APP实例间pinia混乱)
前端·架构