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

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

相关推荐
不想上班只想要钱23 分钟前
vue3+vite创建的项目,运行后没有 Network地址
前端·javascript·vue.js
流***陌41 分钟前
手办盲盒抽赏小程序前端功能设计:兼顾收藏需求与抽赏乐趣
前端·小程序
岁月宁静1 小时前
在富文本编辑器中封装实用的 AI 写作助手功能
前端·vue.js·人工智能
金士顿1 小时前
为什么MainWindow.xaml绑定的datacontext,EtherCATSuiteCtrl.xaml直接用了?
前端
533_1 小时前
[css] flex布局中的英文字母不换行问题
前端·css
浮游本尊2 小时前
React 18.x 学习计划 - 第四天:React Hooks深入
前端·学习·react.js
future_studio2 小时前
聊聊 Unity(小白专享、C# 小程序 之 日历、小闹钟)
前端·html
Yeats_Liao2 小时前
Go Web 编程快速入门 · 04 - 请求对象 Request:头、体与查询参数
前端·golang·iphone
祈祷苍天赐我java之术3 小时前
Redis 数据类型与使用场景
java·开发语言·前端·redis·分布式·spring·bootstrap
草莓熊Lotso3 小时前
C++ 方向 Web 自动化测试入门指南:从概念到 Selenium 实战
前端·c++·python·selenium