前端深拷贝的简易实现

JS这门语言没有提供深拷贝的内建方法,诸如slice, concat, Object.assign这些方法其实都是对象的浅拷贝,修改深层次引用时就会变更原始数据,这在一些必须使用深拷贝的场景是无法接受的,所以如何实现一个有用又靠谱的深拷贝方法变得至关重要。

如果不想搞得太复杂,可以直接使用这个懒汉大法:JSON.parse(JSON.stringify(target)),它的思路很简单,先序列化再反序列化,得到一个全新的对象。而事实上,在只包含原始数据类型和对象的数据结构中,这种大法是完全可用的。它的主要问题在于:

  1. 无法保证复制后的同一性
  2. 无法处理循环引用

具体的问题可以查看JSON.stringify的文档,就不赘述了。

要手动实现一个深拷贝,则需要对症下药——根据不同的数据类型选择合适的拷贝方案,所以首先我们需要判别数据类型:

const TYPE_PRIMITIVE = 1
const TYPE_UNDEF = 2
const TYPE_SYMBOL = 3
const TYPE_DATE = 4
const TYPE_REGEXP = 5
const TYPE_FUNCTION = 6
const TYPE_ARRAY = 7
const TYPE_PLAIN_OBJECT = 8
const TYPE_UNKNOWN = 9

function getType (target) {
  let type = Object.prototype.toString.call(target).slice(8, -1)
  let innerType = typeof target

  if (['number', 'string', 'boolean'].includes(innerType)) {
    return TYPE_PRIMITIVE
  }
  else if (target === null || target === undefined) {
    return TYPE_UNDEF
  }
  else if (type === 'Date') {
    return TYPE_DATE
  }
  else if (innerType === 'symbol') {
    return TYPE_SYMBOL
  }
  else if (type === 'RegExp' && 'lastIndex' in target) {
    return TYPE_REGEXP
  }
  else if (Array.isArray(target)) {
    return TYPE_ARRAY
  }
  else if (innerType === 'function') {
    return TYPE_FUNCTION
  }
  else if (type === 'Object') {
    return TYPE_PLAIN_OBJECT
  }
  else {
    return TYPE_UNKNOWN
  }
}

我们针对上述数据类型进行拷贝分析:

  1. 原始数据类型(Number, String, Boolean)或者未定义(null, undefined)则直接返回它就好;
  2. Date,先获取它的时间戳,然后依此创建一个新的Date对象即可;
  3. 正则表达式,要分别获取它的源(source)和flags,然后依此创建新的正则式;
  4. 内建函数,直接返回它;
  5. 不是内建函数,则创建一个相同的函数并返回;
  6. Symbol,返回一个与它描述相同的Symbol即可
  7. 对象和数组,需要遍历地拷贝它们的每个属性或索引,而这些属性有可能也是个对象或者数组,所以必须递归地执行拷贝。另外一个问题是循环引用,举个例子 a['x'] = b; b['y'] = a,如果不加限制,copy(a)的递归会一直进行下去直到栈溢出。解决方法是用一个weakMap保存 [原始对象]: [拷贝对象]的映射关系,按上面的例子来说就是,如果执行copy(a), weakMap会依次添加a: copy_ab: copy_b的映射,然后递归子例程copy(b)会继续递归执行copy(a),此时映射关系中存在a的映射,所以直接返回a对应的拷贝对象即可,这样就消除了无限递归。

具体实现:

function isNative (constructor) {
  return typeof constructor === 'function' && /native code/.test(constructor.toString())
}

function copyRegExp(regExp) {
  const flags =  /\w+/.exec(regExp.flags)
  const result = new regExp.constructor(regExp.source, flags ? flags[0] : '')
  result.lastIndex = regExp.lastIndex
  return result
}

function copySymbol(symbol) {
  let description = symbol.description || symbol.toString().slice(7, -1)
  return Symbol(description)
}

function copyFunction(func) {
  if (isNative(func)) return func
  let f = new Function(`return ${func}`)
  return f()
}

function copyProto(orig, dist) {
  let _proto = Object.getPrototypeOf(orig)
  Object.setPrototypeOf(dist, _proto)
  return dist
}

export default function copy(target, withProto, hMap) {
  let type = getType(target)

  switch (type) {
    case TYPE_PRIMITIVE:
    case TYPE_UNDEF:
      return target
    case TYPE_DATE:
      return new Date(target.getTime())
    case TYPE_REGEXP:
      return copyRegExp(target)
    case TYPE_FUNCTION:
      return copyFunction(target)
    case TYPE_SYMBOL:
      return copySymbol(target)
    case TYPE_ARRAY:
      // ignore properties in array
      hMap = hMap || new WeakMap()
      if (hMap.has(target)) {
        return hMap.get(target)
      }
      else {
        let mirList = []
        hMap.set(target, mirList)
        for (let i = 0; i < target.length; i++) {
          mirList.push(copy(target[i], withProto, hMap))
        }
        return withProto ? copyProto(target, mirList) : mirList
      }
    case TYPE_PLAIN_OBJECT:
      hMap = hMap || new WeakMap()
      if (hMap.has(target)) {
        return hMap.get(target)
      } else {
        let mirObj = {}
        hMap.set(target, mirObj)
        // ignore properties not enumerable
        // ignore Symbol keys
        let keys = Object.keys(target)
        for (let i = 0; i < keys.length; i++) {
          let key = keys[i]
          mirObj[key] = copy(target[key], withProto, hMap)
        }
        return withProto ? copyProto(target, mirObj) : mirObj
      }
    default:
      console.warn('can\'t detect type of the target: ' + target)
      return target
  }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值