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

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

相关推荐
中微子3 小时前
虚拟列表完全指南:从零到一手写实现
前端
jqq6663 小时前
解析ElementPlus打包源码(二、buildFullBundle)
前端·javascript·vue.js
YaeZed3 小时前
TypeScript6(class类)
前端·typescript
织_网3 小时前
UniApp 页面通讯方案全解析:从 API 到状态管理的最佳实践
前端·javascript·uni-app
emojiwoo4 小时前
前端视觉交互设计全解析:从悬停高亮到多维交互体系(含代码 + 图表)
前端·交互
xxy.c4 小时前
嵌入式解谜日志—多路I/O复用
linux·运维·c语言·开发语言·前端
yuehua_zhang4 小时前
uni app 的app端 写入运行日志到指定文件夹。
前端·javascript·uni-app
IT_陈寒5 小时前
SpringBoot 3.x实战:5种高并发场景下的性能优化秘籍,让你的应用快如闪电!
前端·人工智能·后端
麦文豪(victor)5 小时前
自动化流水线
前端