js实现深拷贝(深克隆)

关于深拷贝和浅拷贝的概念和区别在这里就不再赘述了,
而常规的JSON.parse(JSON.stringfy(data)方式存在很多缺陷,例如无法处理undefined、function、特殊引用类型、循环引用等。

最近尝试手写一下深拷贝的实现,分享一下思路和代码。(完整代码见文章末尾)

一、整体思路

深拷贝要考虑的点还是挺复杂的,数据类型太多需要一一处理,具体我是怎么一步步手写以及修改填坑的过程就不多说了,就大概说一下我的代码流程吧。
(定义源数据为target,克隆后的数据为result)

1、数据类型划分处理

  • 数据类型的判断就通过这个方法即可:

    const type = Object.prototype.toString.call(target).match(/\s(\w+)\]/)[1]
    

    返回数据target的类型,首字母大写,之所以返回首字母大写的格式也是为了后面通过new构造函数的方式生成拷贝实例。

  • 先区分基本数据类型和复杂数据类型,基本数据类型直接返回结果:

    if (typeof target !== 'object' || target === null) {
      // 基本数据类型
      result = target
    }
    
  • 针对复杂数据类型再划分成两类,一类是需要递归拷贝的类型:

    if (['Array', 'Set', 'Map', 'Object', 'Arguments'].includes(type)) {}
    
  • 另一类是不需要递归的类型,也就是其他所有类型,逐个做处理赋值。

2、递归处理

针对'Array', 'Set', 'Map', 'Object', 'Arguments'这五中类型需要做递归深度遍历,写递归的核心就是做好变量传参和递归出口,将已处理的结果作为参数递归传入,然后定义递归的出口防止死循环。
只是不同的类型遍历的方式稍有不同:

  // Array
  target.forEach(v => {
    result.push(clone(v))
  })

  // Set
  target.forEach(v => {
    result.add(clone(v))
  })

  // Map
  target.forEach((v, k) => {
    result.set(k, clone(v))
  })

  // Object Arguments
  Object.keys(target).forEach(k => {
    result[k] = clone(target[k])
  })

不要使用for…in遍历,因为它会遍历对象的所有可枚举属性,包括原型链上的,原型链上的属性不应该拷贝到新对象自身属性上。

二、特殊处理

1、拷贝结果的初始化

意思就是拷贝赋值时是通过什么方式,比如拷贝一个对象,推荐不使用result = {}的字面量方式,而是采用new构造函数的方式来初始化result,即result = new Object()
这样做的好处就是能保持原始构造函数的原型和继承信息,比如通过es6的class形式创造的Person类,通过对象字面量初始化拷贝时,访问它的构造函数是Object,而通过new初始化拷贝后访问构造函数是Person。

const Constr = target.constructor
result = new Constr()

2、循环引用的处理

循环引用即源数据中可能存在内部数据互相引用的问题,如不处理在递归的时候会导致死循环。

处理循环引用可以利用map或weakMap数据结构很巧妙的实现,即每次处理需递归的类型时都把当前要递归的子数据作为key、把result结果作为value写进map里,然后在递归之前先检查一下要递归的数据是否已存在于map中,如果已存在就直接取出value返回。

if (map.get(target)) {
  result = map.get(target)
} else {
  const Constr = target.constructor
  result = new Constr()
  map.set(target, result)
  // 先给map赋值,在下面写常规的递归逻辑
}

3、不需要递归的类型

(1)正则
if (type === 'RegExp') {
 // RegExp
  result = new Constr(target.source, /\w*$/.exec(target))
  result.lastIndex = target.lastIndex
}
(2)函数

函数暂时没发现有需要拷贝的场景,再加上函数柯里化的形式难以处理,所以简单点直接赋值返回。

if (type.includes('Function')) {
  // Function AsyncFunction GeneratorFunction
  result = target
}
(3)错误对象
if (type === 'Error') {
  // Error
  result = new Constr(target.message)
  result.stack = target.stack
}
(4)包装过的基本数据类型

例如new Number(1)、Object(Symbol(1))等,包括未具体判断的类型都统一处理。

else {
  try {
    // 包装过的 Number String Symbol BigInt
    const val = Constr.prototype.valueOf.call(target)
    result = Object(val)
  } catch (err) {
    // other
    console.warn(`Uncatched type:${type}`)
    console.warn(err)
  }
}

可能还有一些没有考虑到的数据类型需要做特殊处理,以后遇到了再更。

三、完整代码

1、函数实现

/** 
 * @description: 深克隆方法
 * @param {any} target 源数据
 * @return {any} 克隆后的数据
 */
function deepClone (target) {
  function clone (target, map = new WeakMap()) {
    let result
    const type = Object.prototype.toString.call(target).match(/\s(\w+)\]/)[1]
  
    if (typeof target !== 'object' || target === null) {
      // 基本数据类型
      result = target
    } else {
      if (['Array', 'Set', 'Map', 'Object', 'Arguments'].includes(type)) {
        // 可递归遍历的类型处理
        // 循环引用处理
        if (map.get(target)) {
          result = map.get(target)
        } else {
          const Constr = target.constructor
          result = new Constr()
          map.set(target, result)
          
          if (type === 'Array') {
            // Array
            target.forEach(v => {
              result.push(clone(v, map))
            })
          } else if (type === 'Set') {
            // Set
            target.forEach(v => {
              result.add(clone(v, map))
            })
          } else if (type === 'Map') {
            // Map
            target.forEach((v, k) => {
              result.set(k, clone(v, map))
            })
          } else {
            // Object Arguments
            Object.keys(target).forEach(k => {
              result[k] = clone(target[k], map)
            })
          }
        }
      } else {
        // 不可递归遍历的类型处理
        const Constr = target.constructor
        if (type === 'RegExp') {
          // RegExp
          result = new Constr(target.source, /\w*$/.exec(target))
          result.lastIndex = target.lastIndex
        } else if (type.includes('Function')) {
          // Function AsyncFunction GeneratorFunction
          result = target
        } else if (['Date'].includes(type)) {
          // Date
          result = new Constr(target)
        } else if (type === 'Error') {
          // Error
          result = new Constr(target.message)
          result.stack = target.stack
        } else if (type === 'URL') {
          // URL
          result = new Constr(target.href)
        } else if (type.includes('Array')) {
          // ArrayBuffer TypeArray BigArray ...
          result = target.slice()
        } else if (type === 'DataView') {
          // DataView
          result = new Constr(target.buffer.slice(0), target.byteOffset, target.byteLength)
        } else {
          try {
            // 包装过的 Number String Symbol BigInt
            const val = Constr.prototype.valueOf.call(target)
            result = Object(val)
          } catch (err) {
            // other
            console.warn(`Uncatched type:${type}`)
            console.warn(err)
          }
        }
      }
    }
    return result
  }

  const res = clone(target)
  return res
}

2、测试代码

/**
 * 测试
 */
const target = {
  a: {
    b1: 2,
    b2: 5,
  },
  b: [1 , 2, { c1: 3, c2: 4 }],
  c: 1,
  d: Symbol(123),
  e: undefined,
  f: null,
  g: () => {},
  h: new Date(),
  i: /123/gi,
  j: Object(Symbol(45)),
  k: new Error('wrong'),
  l: new URL('https://www.baidu.com:80/#/index'),
  m: new Number(12),
  n: new String('23'),
  o: new Boolean('23'),
  p: BigInt('23'),
  q: Object(BigInt('23')),
  r: new ArrayBuffer(10),
  s: new Int8Array(10),
}
target.z = { ref: target }

const result = deepClone(target)
console.log(result)

参考链接:https://juejin.cn/post/6844903929705136141

你可能感兴趣的:(html/css/js,javascript,深拷贝,深克隆,循环引用,面试)