lxinr/interview-question

2021/02/24 - 深浅拷贝的实现

Opened this issue · 0 comments

lxinr commented

概念

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,即引用类型会共享内存地址

  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象,即引用类型不会共享内存地址

浅拷贝

Object.assign

用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象

const sourceInfo = {
  v: 3,
  value: {
    key: '222'
  },
  arr: [4, 6, 9]
}

const copy = Object.assign({}, sourceInfo)

sourceInfo.v = 6
sourceInfo.value.key = 999

console.log(sourceInfo.v === copy.v) // false
console.log(sourceInfo.value.key === copy.value.key) // true
扩展运算符...
const sourceInfo = {
  v: 3,
  value: {
    key: '222'
  },
  arr: [4, 6, 9]
}

const copy = { ...sourceInfo }

sourceInfo.v = 6
sourceInfo.value.key = 999

console.log(sourceInfo.v === copy.v) // false
console.log(sourceInfo.value.key === copy.value.key) // true
Array.prototype.concat()(数组有效)
const sv = [1, 6, 40, {
  b: 45
}]

const cv = sv.concat()

sv[1] = 9
sv[3].b = '666'

console.log(sv[1] === cv[1]) // false
console.log(sv[3].b === cv[3].b) // true
Array.prototype.slice()(数组有效)
const sv = [1, 6, 40, {
  b: 45
}]

const cv = sv.slice()

sv[1] = 9
sv[3].b = '666'

console.log(sv[1] === cv[1]) // false
console.log(sv[3].b === cv[3].b) // true
lodash中的clone方法

深拷贝

JSON.parse(JSON.stringify())

序列化反序列化方法

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,且当需拷贝的对象存在循环引用时会报错

const testInfo = {
  f: 3,
  msg: function () {
    return '哈哈哈哈'
  },
  v: '34523423',
  set: new Set(),
  res: {
    data: 4567
  }
}

const ct = JSON.parse(JSON.stringify(testInfo))
testInfo.f = 8
testInfo.res.data = 8888


console.log(testInfo.f === ct.f) // false
console.log(testInfo.res.data === ct.res.data) // false

手写实现

目前仅使用递归实现,未处理可能爆栈的情况

/** 
 * 1. 如果是原始类型,无需继续拷贝,直接返回
 * 2. 如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性执行深拷贝后依次添加到新对象上
 * 3. 针对一些特定的类型特殊处理,如Set,Map
 * 4. 简单处理:除Array,Object,Map,Set外,其余类型都直接返回
 * 
 * 优化点
 * 1. 使用WeakMap来解决循环引用导致的问题
 * 2. 使用循环的方式来解决迭代所可能导致的爆栈问题
 * 
 * 深入 js 深拷贝对象 - https://www.jianshu.com/p/b08bc61714c7
 * 深拷贝的终极探索(90%的人都不知道) - https://juejin.cn/post/6844903692756336653
 * 如何写出一个惊艳面试官的深拷贝? - https://segmentfault.com/a/1190000020255831
 */

const arrayTag = '[object Array]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const functionTag = '[object Function]'
const nullTag = '[object Null]'
const undefinedTag = '[object Undefined]'

// 需遍历拷贝的类型
const quoteTags = [arrayTag, objectTag, mapTag, setTag]

export function typeTag(target) {
  return Object.prototype.toString.call(target)
}

/**
 * 深拷贝方法
 *
 * @author liux
 * @date 2021-02-21
 * @export
 * @param {*} target
 * @param {*} [map=new WeakMap()]
 * @returns 
 */
export function cloneDeep(target, map = new WeakMap()) {
  // 因WeakMap的键为弱引用,可被垃圾回收,用于解决循环引用的问题
  let cloneTarget

  const targetType = typeTag(target)

  function initTarget(target, type) {
    const Ctor = target.constructor // 得到实例对象的引用
    // 使用Object.create(null)创建的的对象没有constructor
    if(!Ctor && type === objectTag) return new Object
    return new Ctor()
  }


  // 是否是需迭代处理的类型
  function isQuote(obj) {
    return quoteTags.indexOf(typeTag(obj)) !== -1
  }

  function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index)
    }
    return array
  }
  // 如果不是需遍历的类型,则直接返回
  if(!isQuote(target)) {
    return target
  } else {
    cloneTarget = initTarget(target, targetType)
    if(map.has(target)) return map.get(target)

    map.set(target, cloneTarget)
    // 处理数组和对象
    if(targetType === objectTag || targetType === arrayTag) {
      for(let key in target) {
        cloneTarget[key] = cloneDeep(target[key], map)
      }

      // 或使用while来处理,能适当提高效率
      // const keys = isArray ? undefined : Object.keys(target)
      // forEach(keys || target, (value, key) => {
      //   if (keys) key = value
      //   cloneTarget[key] = cloneDeep(target[key], map)
      // })
    }

    // 处理Map
    if(targetType === mapTag) {
      target.forEach((subTarget, key) => {
        cloneTarget.set(key, cloneDeep(subTarget, map))
      })
    }
    // 处理Set
    if(targetType === setTag) {
      target.forEach(subTarget => {
        cloneTarget.add(cloneDeep(subTarget, map))
      })
    }
  }

  return cloneTarget

}
lodash库中的cloneDeep方法