xxleyi / learning_list

聚集自己的学习笔记
10 stars 3 forks source link

deep clone in JS #260

Open xxleyi opened 3 years ago

xxleyi commented 3 years ago

前端面试有一个高频考点:对象深拷贝。

这是一个典型的开放题,有很多很多点,几乎不可能完全覆盖,体现的是个人对未知问题的界定与思考。

我自己的思考如下:

既然是对象子类型 ,那就需要统一枚举 for(let key in obj) ...,至于原型链上的问题,可以考虑通过技术手段进行规避。

然后针对函数,如何拷贝呢?实现语义上的拷贝即可,在外面包一层普通函数或箭头函数,使用 toString 区分是否是普通函数。

然后针对 Set 和 Map 还需要将迭代部分的 v 或 k, v 补进去,同时需要注意,Map 类型的数据,其 key 也可能是对象,是否应该深拷贝呢?应该也需要。

总而言之,需要考虑和权衡的东西蛮多,代码逻辑可以遵循主干,写一个后续无需修改,只需扩展的处理逻辑。

我尝试写了这么一个:

const toStringTag = (v) => Object.prototype.toString.call(v).slice(8, -1)

function cloneFunction(fun) {
    if (fun.toString().startsWith('function')) return function(...args) { return fun.call(this, ...args) }
    return (...args) => fun(...args)
}

function clone(target, map = new Map()) {
    const stringTag = toStringTag(target)
    // 如果是基本数据类型,则直接返回
    if (!(target instanceof Object)) return target
  // 否则处理 ['Array', 'Object', 'Set', 'Map', 'Function'] 以及更多,比如 String, Number, Date, RegExp
  // 更多情况默认返回 target
  let deepContainers = {
    'Array': [],
    'Object': {},
    'Set': new Set(),
    'Map': new Map(),
    'Function': stringTag === 'Function' ? cloneFunction(target) : null,
  }

  for (let k of ['String', 'Number', 'Date', 'RegExp']) {
    deepContainer[k] = new target.constructor(target)
  }

  const deepContainer = deepContainers[stringTag] || target
  // 意料之外的对象子类型,当作不可变数据处理
  if (deepContainer === target) return target

    if (map.get(target)) return map.get(target)
    map.set(target, deepContainer)

    for (let key in target) deepContainer[key] = clone(target[key], map)

    switch (stringTag) {
        case 'Set':
            for (let v of target) deepContainer.add(clone(v, map))
            break 
        case 'Map':
            for (let [k, v] of target) deepContainer.set(clone(k, map), clone(v, map))
            break 
    }

    return deepContainer
}

后续只需要扩展 deepContainers 这个映射关系以及相应的 cloneSubObject 函数即可。

比如,想要深拷贝 Date 类型,则只需要添加这么一条映射关系:

{ 'Date': stringTag === 'Date' ? cloneDate(target) : null }

以及相应的克隆函数:

function cloneDate(date) {
    return new Date(+date)
}

再比如,想要深拷贝 RegExp 类型,则只需要添加这么一条映射关系:

{ 'RegExp': stringTag === 'RegExp' ? cloneRegExp(target) : null }

以及相应的克隆函数:

function cloneRegExp(reg) {
    return new RegExp(reg.valueOf())
}

经过上面这两个例子,似乎有了通用的模式:new target.constructor(target.valueOf()) 来对付这类子对象类型。

经过我进一步研究,对于 Date 和 RegExp 类型来说,直接 new target.constructor(target) 就可以实现拷贝,然后重点是就是枚举 key 然后进行深拷贝。


更新:分析奇怪代码时,尝试使用了一下这个深拷贝,遇到一些问题:浏览器内置的一些数据类型,如 navigator.permissions PermissionStatus 会导致我的程序报错,需要改进一番。改进措施就是不要全部使用 new target.constructor(target),这一招应该只适用于 String, Number, Date, RegExp