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

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

相关推荐
架构技术专栏1 小时前
Agent 工具调用:从工具声明到执行反馈
前端·人工智能·后端
别动我齐刘海2 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
志尊宝2 小时前
Vue3 零基础每日笔记(071):暗黑模式与主题切换——CSS 变量 + Element Plus 暗黑方案
前端·css·笔记·vue·html·前端开发·软件开发
刘发财3 小时前
前端如何生成10000页PDF文件?
前端·javascript·开源
A接拉起0073 小时前
2026 年 JavaScript 开发者需要了解的知识
前端·javascript·后端
_馒头3 小时前
pixiJs游戏开发实践 - 跳棋小游戏
javascript·vue.js
用户11481867894844 小时前
面试官:大屏项目里十几个模块同时渲染上万条数据,怎么做到不卡?
前端·性能优化
前端市界4 小时前
Pro React Admin: 统一工程底座,承载多种应用...
前端·github
葡萄城技术团队4 小时前
dsh-spreadjs-editor 当前支持什么:安装前需要了解的几件事
前端
三*一4 小时前
优化修整算法
前端·javascript·算法