阅读 532

前端面试系列【003】 - 什么是深拷贝,和浅拷贝有什么区别,动手实现一个深拷贝

要回答这道题,我们需要搞清楚下面的问题:

  • 什么是浅拷贝?
  • 什么是深拷贝?
  • 二者有什么区别?
  • 如何实现?

那么便开始吧(๑•̀ㅂ•́)و✧!

由来

首先,我们知道在 js 中变量分为两种基础类型引用类型两种。当我们将一个基础类型的变量赋值给另一个变量的时候,会在内存中新开辟一块空间来存放,而我们将引用类型赋值给一个新的变量的时候,实际上只是让这两个变量都指向了同一块内存。

这个时候如果我们修改 objA,是能影响到 objB 的:

明明修改的是 objA,却影响到了 objB,显然这是我们不希望看到的,由此就衍生出了拷贝技术:在赋值的时候即使目标是引用类型,也在内存中新开辟一块空间,将原始数据原封不动的拷贝过去,从而让我们再修改 objA 的时候并不会影响到 objB。

浅拷贝

所谓浅拷贝,就是浅层次的对数据进行拷贝。

回到上面的例子,如果用浅拷贝将 objA 赋值给 objB,那么无论 objA 如何改变,都不会影响到 objB。

但如果 objA 中存在引用类型呢?

const objA = {
  name: 'Alice',
  age: 18,
  family: {
    mother: 'Anna',
    futher: 'Jonh',
  },
}
复制代码

我们知道,拷贝是原封不动的将数据拷贝过去,所以这里 family 也会同样的拷贝过去,但由于它本身是一个引用类型,所以拷贝的也是指向同一块内存的指针,这个时候如果对 family 进行修改,objB 中的 family 显然是会被影响到的。

常用的浅拷贝

虽然浅拷贝存在上述隐患,却也是非常常用的技术,在日常工作中,除了自己造轮子以外,还有以下方法能够实现浅拷贝:

  • ES6 扩展运算符
  • Object.assign
  • Array.prototype.slice
  • Array.prototype.concat

深拷贝

既然浅拷贝是浅层次的对数据进行拷贝,那么顾名思义,深拷贝自然是深层次的对数据进行拷贝。无论被拷贝的对象的元素包含 ObjectArray 还是 DatePromise,甚至自身,深拷贝都应当能够完好的将数据完全复制到新开辟的内存空间中,这样,无论对原始对象进行任意修改,都不会影响到新生对象:

二者存在什么区别

区别主要在于拷贝的层次不同:

  • 浅拷贝仅对数据的第一层进行拷贝,对于深层的数据依然是简单的使用赋值语句
  • 深拷贝将递归遍历整个对象,对其所有数据进行完全拷贝

如何实现

实现深拷贝的核心思路比较简单:递归遍历原始对象,以基本类型为最小粒度进行赋值

这样一看似乎深拷贝实现起来也并不困难,但实际上需要考虑的点要更多一些:

  • 不同类型的处理:Object, Array, Function, RegExp, Date, Promise...
  • 循环引用:objA.me = objA

对于循环引用,这里我们可以利用 weakMap<原对象引用, 新对象引用>:如果下次递归中判断当前引用等于原对象引用,则直接返回新创建对象的引用即可。

而对于不同对象的处理,我们只需要按照不同对象进行构造即可,所以这里我们先假定对象中只有 Object,来实现带有循环引用的深拷贝,之后再增加类型。

function deepCopy(obj, me = new WeakMap()) {
  if (!isObject(obj)) return obj
  if (me.get(obj)) return me.get(obj)

  let copy,
    result = {}
  me.set(obj, result)
  Object.keys(obj).forEach((key) => {
    copy = obj[key]
    result[key] = isObject(copy) ? deepCopy(copy, me) : copy
  })
  return result
}
复制代码

接下来看看这段代码是否能够如期完成需求:

显然,利用 WeakMap 可以成功解决了最头疼的循环引用问题。

当然,这里还有别的方法:比如维护两个循环引用的数组。但最简单直接的还是 WeakMap

完成了最核心的功能,接下来就是扩展类型了。

function isObject(obj) {
  return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function isArrary(obj) {
  return Array.isArray(obj)
}
function isDate(obj) {
  return Object.prototype.toString.call(obj) === '[object Date]'
}
function isSet(obj) {
  return Object.prototype.toString.call(obj) === '[object Set]'
}
function isMap(obj) {
  return Object.prototype.toString.call(obj) === '[object Map]'
}
function isRegExp(obj) {
  return Object.prototype.toString.call(obj) === '[object RegExp]'
}
function isFunction(obj) {
  return typeof obj === 'function'
}
function arrayCopy(obj) {
  const res = []
  for (const val of obj) {
    res.push(val)
  }
  return res
}
function dateCopy(obj) {
  const res = new Date(obj)
  return res
}
function setCopy(obj, me) {
  const res = new Set()
  obj.forEach((val) => {
    res.add(deepCopy(val, me))
  })
  return res
}
function mapCopy(obj, me) {
  const res = new Map()
  obj.forEach((val, key) => {
    res.set(key, deepCopy(val, me))
  })
  return res
}
function regExpCopy(obj) {
  function getRegExpInfo(r) {
    let flags = ''
    if (r.global) flags += 'g'
    if (r.ignoreCase) flags += 'i'
    if (r.multiline) flags += 'm'
    return flags
  }
  const res = new RegExp(obj.source, getRegExpInfo(obj))
  if (obj.lastIndex) res.lastIndex = obj.lastIndex
  return res
}
function functionCopy(obj) {
  function createFunction(args, body) {
    if (body) {
      if (args) {
        const argsArr = args[0].split(',')
        return new Function(...argsArr, body[0])
      } else {
        return new Function(body[0])
      }
    }
  }
  function createArrowsFunction(funcString) {
    return eval(funcString)
  }
  const bodyReg = /(?<={)(.|\n)+(?=})/m
  const argsReg = /(?<=\().+(?=\)\s+{)/
  const funcString = obj.toString()

  const body = bodyReg.exec(funcString)
  const args = argsReg.exec(funcString)

  return obj.prototype ? createFunction(args, body) : createArrowsFunction(funcString)
}
function deepCopy(obj, me = new WeakMap()) {
  if (!isObject(obj)) return obj
  if (me.get(obj)) return me.get(obj)

  let result, copy
  if (isArrary(obj)) {
    result = arrayCopy(obj)
    me.set(obj, result)
    return result
  }

  if (isDate(obj)) {
    result = dateCopy(obj)
    me.set(obj, result)
    return result
  }

  if (isSet(obj)) {
    result = setCopy(obj, me)
    me.set(obj, result)
    return result
  }

  if (isMap(obj)) {
    result = mapCopy(obj, me)
    me.set(obj, result)
    return result
  }

  if (isRegExp(obj)) {
    result = regExpCopy(obj)
    me.set(obj, result)
    return result
  }

  if (isFunction(obj)) {
    result = functionCopy(obj)
    me.set(obj, result)
    return result
  }

  result = {}
  me.set(obj, result)
  Object.keys(obj).forEach((key) => {
    copy = obj[key]
    result[key] = isObject(copy) ? deepCopy(copy, me) : copy
  })
  return result
}
复制代码

结语

更佳阅读体验:003 - 什么是深拷贝,和浅拷贝有什么区别,动手实现一个深拷贝

文章分类
前端
文章标签