阅读 532

JavaScript 中的深拷贝

什么是深拷贝?

JavaScript中存在三种可执行代码,global,function,eval。可执行代码的执行依赖于执行上下文。我们可以抽象的将执行上下文理解为一个对象。这个对象上会包含一些属性,如variable object(变量对象),this value(this指针),scope chain(作用域链)。

JavaScript中的基础数据类型都是存储在变量对象中。JavaScript中共有5种基础数据类型,Undefined、Null、Boolean、Number、String。我们可以直接操作存储在变量对象中的,基础数据类型。

JavaScript中的引用数据类型的值,是保存在堆内存中的。我们不能直接操作堆内存空间。我们通过操作variable object中的数据的引用,访问修改对象。这里的应用是堆内存空间中的地址。

image

当我们使用 var a = b赋值的时候,如果b是引用类型,我们赋于a的只是内存空间的地址。

image

这就是产生了一个问题,我们该如何复制引用类型的对象。

常见的深拷贝的方式

JSON.stringify 和 JSON.parse

最简单的方式将对象使用JSON.stringify将对象序列化为JSON格式的字符串,然后使用JSON.parse将JSON字符串反序列化为对象。

image

for in 配合 递归

另外一种方式是使用for in循环,配合递归

image

深拷贝中遇到的问题

到这里,问题解决了吗?并没有,上面方式有以下的问题

  1. JSON以及for…in无法对Date,正则,Set,Map等数据结构做出正确的处理
  2. 没有对Symbol的属性做出正确的处理。
  3. for…in没有对对象中可能存在的循环引用做出正确的处理。

如何解决这些问题?

对于上面问题的解决, 我参考了lodash的源码,以及ramda的源码。lodash的源码中涉及到非常多的边界条件的处理,可读性相对差一些。我在此基础上做出了一定的简化,具体代码在文末。

Date的的处理

当我们检测到value的为[object Date]类型的时候,我们通过Date实例的constructor属性,获取实例的构造函数。Date的构造函数可以接收一个Date的实例作为参数。从而创建一个新的Date实例。

image

Map,Set的处理

和Date对象同理,使用Map或者Set实例的constructor属性,创建新的实例后,通过forEach对Map和Set进行遍历,重新设置Map以及Set的内容。

image

image

正则的处理

  • source,正则对象的源模式文本
  • global, global属性表明正则表达式是否使用了"g"标志
  • ignoreCase, ignoreCase属性表明正则表达式是否使用了"i"标志
  • multiline, multiline属性表明正则表达式是否使用了"m"标志
  • sticky, sticky属性表明正则表达式是否使用了"y"标志
  • unicode, unicode属性表明正则表达式带有"u"标

image

循环引用的处理

当出现循环引用的时候,如果不进行判断,使用递归。会爆栈

image

WeakMap支持使用引用类型作为key,我们可以利用这个特性,将对象的每一个引用类型的属性作为key,存储在WeakMap中。当同样的key再次出现时,可以证明发生了循环引用。直接返回结果。

image

image

Symbol属性的处理

Symbols类型的属性无法使用Object.keys获取,可以通过Object.getOwnPropertySymbols方法获取对象上的Symbol类型的属性。

image

参考

源码


// 使用WeakMap判断是否形成了环,避免爆栈
const hash = new WeakMap()

function hasHash (value) {
  return hash.has(value)
}

function setHash (value, result) {
  hash.set(value, result)
}

function getHash (value) {
  return hash.get(value)
}

function getSymbols (value) {
  let symKeys = []
  const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
  const nativeGetSymbols = Object.getOwnPropertySymbols
  symKeys = nativeGetSymbols(value)
  // 判断是否是可枚举的属性
  symKeys = symKeys.filter(symkey => propertyIsEnumerable.call(value, symkey))
  return symKeys
}

function getAllKeys (value) {
  let keys = Object.keys(value)
  keys = [...keys, ...getSymbols(value)]
  return keys
}

const types = {
  '[object Array]': true,
  '[object Boolean]': true,
  '[object Date]': true,
  '[object Map]': true,
  '[object Set]': true,
  '[object Number]': true,
  '[object Object]': true,
  '[object RegExp]': true,
  '[object Symbol]': true,
  '[object String]': true,
  '[object ArrayBuffer]': true,
  '[object Function]': true,
  '[object WeakMap]': false,
  '[object Error]': false
}

function isObject (value) {
  const type = typeof value;
  return value != null && (type === 'object' || type === 'function')
}

function getType (value) {
  return Object.prototype.toString.call(value);
}

function initCloneArray (value) {
  const { length } = value
  return new value.constructor(length)
}

function initCloneArrayBuffer (value) {
  const result = new value.constructor(value.byteLength)
  new Uint8Array(result).set(new Uint8Array(value))
  return result
}

function initCloneObject (value) {
  return Object.create(Object.getPrototypeOf(value))
}

function initCloneRegExp (value) {
  return new RegExp(value.source,
    (value.global     ? 'g' : '') +
    (value.ignoreCase ? 'i' : '') +
    (value.multiline  ? 'm' : '') +
    (value.sticky     ? 'y' : '') +
    (value.unicode    ? 'u' : '')
  )
}

function initCloneFunction (value) {
  return function (...args) {
    return value.apply(null, ...args)
  }
}

function initClone (value, type) {
  const Ctor = value.constructor
  switch (type) {
    case '[object ArrayBuffer]':
      return initCloneArrayBuffer(value)
    case '[object Date]':
    case '[object Map]':
    case '[object Set]':
      return new Ctor(value)
    case '[object RegExp]':
      return initCloneRegExp(value)
    case '[object Array]':
      return initCloneArray(value)
    case '[object Object]':
      return initCloneObject(value)
    case '[object Function]':
      return initCloneFunction(value)
  }
}

export default function deepClone (value) {

  let result;

  const type = getType(value)

  // 如果不是引用类型直接返回
  if (!isObject(value)) {
    return value
  }

  // 如果是weakMap,Error类型直接返回
  if (!types[type]) {
    return value
  }

  let isArr = Array.isArray(value)

  result = initClone(value, type)

  // 判断是否产生了循环引用, 避免爆栈
  if (hasHash(value)) {
    // 如果存在直接返回
    return getHash(value)
  }
  // 在weakMap添加标记
  setHash(value, result)

  if (type === '[object Map]') {
    value.forEach((val, key) => {
      result.set(key, deepClone(val))
    })
    return result
  }

  if (type === '[object Set]') {
    value.forEach((val, key) => {
      result.add(deepClone(val))
    })
    return result
  }

  const props = getAllKeys(value)

  for (let i = 0; i < props.length; i++) {
    const key = props[i]
    const val = value[key]
    result[key] = deepClone(val)
  }

  return result
}

复制代码
关注下面的标签,发现更多相似文章
评论