liangbus / blogging

Blog to go
10 stars 0 forks source link

关于深拷贝 #11

Open liangbus opened 4 years ago

liangbus commented 4 years ago

前几天看到掘金一个人发帖说一行代码实现 deepClone(记得好像还是个企业号), 点进去果然一片骂声,没错,就是通过 stringify 实现的。

JSON.stringify

骂他其实是因为他的文章太水,并非用 stringify 百分百是错的,假如不考虑安全性,不考虑 function,undefined,Symbol 这些值

MDN 里面说明了

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)

JSON.stringify 还是能满足简单的要求的

var o = {a : 1234, b: {c: [1,2,3,4]}}
JSON.stringify(o) // "{"a":1234,"b":{"c":[1,2,3,4]}}"

var o2 = {a : 1234, b: {c: [1,2,3,4]}, d: () => {alert(0)}, e: undefined}
JSON.stringify(o2) // "{"a":1234,"b":{"c":[1,2,3,4]}}"

var o3 = {a : 1234, b: {c: [1,2,3,4]}, d: () => {alert(0)}, e: undefined, date: new Date()}
JSON.stringify(o3) // "{"a":1234,"b":{"c":[1,2,3,4]},"date":"2019-11-27T03:19:56.810Z"}" Date 对象会自动调用 toJSON 方法转换成字符串

递归

上面的是简单版本,我们再来完善一下,要能够兼容上面所不支持的一些类型 通过递归,很简单就能实现,直接看代码

function cloneDeep(obj) {
  const copy = {}
  for(let k in obj) {
    let item = obj[k]
    if(Array.isArray(item)) {
      copy[k] = [].concat(item)
    } else if (typeof item === 'object') {
      copy[k] = cloneDeep(item)
    } else {
      copy[k] = item
    }
  }
  return copy
}
var foo = {
    x: 1,
    y: undefined,
    z: (x, y) => { return x + y },
    a: Symbol("bar"),
    b: [1, 2, 3, 4]
}
cloneDeep(foo) // {x: 1, y: undefined, a: Symbol(bar), b: Array(4), z: ƒ}

普通的 Object 类型数据,用上面的是基本够用了

那什么时候会不够用??? 对象里面出现循环引用的时候,递归会找不到边界而陷入无穷递归,出现爆栈

Uncaught RangeError: Maximum call stack size exceeded

加入 WeakMap 缓存对象

针对上述问题,加一个对象存储起来就好了,前阵子刚学的 WeakMap, WeakSet 正好排上了用场

/**
 * 深拷贝
 * @param target 拷贝目标
 * @param hash 存放对象的 map,防止循环引用
 */
function cloneDeep(target, hash = new WeakMap()) {
  // 基本类型或者 function 类型的直接返回值
  if (typeof target !== 'object' || hash.has(target)) return target
  // 数组则通过 concat copy
  if(Array.isArray(target)) {
    hash.set(target, target)
    return [].concat(target)
  } else if (typeof target === 'object') { // 对象类型
    // 遍历对象中包含 Symbol 类型的所有 key 值,存到一个累加器当中
    return [...Object.keys(target), ...Object.getOwnPropertySymbols(target)].reduce((res, curKey) => {
      if (typeof target[curKey] === 'object') {
        hash.set(target[curKey], target[curKey])
      }
      res[curKey] = cloneDeep(target[curKey], hash)
      return res
      // 初始化参数,假如迭代的对象非最外层级,则以当前对象的原型创建一个新的对象作为下一层递归的初始值
    }, target.constructor === Object ? {} : Object.create(target.constructor.prototype))
  }
}

到这里,一个深拷贝函数已经基本实现了,基本上能适用于平常的开发场景,但是看了一下 lodash 里面的 cloneDeep 的源码之后,发现事情并没有那么简单,应该还是有相当一部分边界没有覆盖到吧,如果为保险起见,用 lodash 里面的吧,省事~

参考: JSON.stringify() - MDN

liangbus commented 4 years ago

翻车了 对于数组数据的拷贝,上面的代码是有问题的, 因为 [].concat 只做了一层浅拷贝 测试:

var arr1 = [{a: 1, b: 2, c: 3}, {d: 4, e: 'hey hey'}, {f: false, g: []}]
var copy1 = cloneDeep(arr1)
copy1[0] === arr1[0] // true
var t = arr1[1]
t.e = 'string changed'
copy1[1] // {d: 4, e: "string changed"}

可见,这里仅仅是拷贝了其内存地址,并不是真正的拷贝其内容

经修改验证后的代码

/**
 * 深拷贝
 * @param target 拷贝目标
 * @param hash 存放对象的 map,防止循环引用
 */
function cloneDeep(target, hash = new WeakMap()) {
  // 基本类型或者 function 类型的直接返回值
  if (typeof target !== 'object' || hash.has(target)) return target
  // 数组类型
  if(Array.isArray(target)) {
    const res = []
    for(let item of target){
      if(typeof item === 'object') {
        res.push(cloneDeep(item))
      } else {
        res.push(item)
      }
    }
    // hash.set(target, target)
    // 此处不能直接用 concat,concat 是浅拷贝,如果元素是对象类型,只会拷贝其地址
    // return [].concat(target)
    return res

  } else if (typeof target === 'object') { // 对象类型
    // 遍历对象中包含 Symbol 类型的所有 key 值,存到一个累加器当中
    return [...Object.keys(target), ...Object.getOwnPropertySymbols(target)].reduce((res, curKey) => {
      if (typeof target[curKey] === 'object' && !Array.isArray(target[curKey])) {
        // 这里只有 object 对象才会有循环引用的可能,需排除数组类型,否则对象下的数组仍然是浅拷贝
        hash.set(target[curKey], target[curKey])
      }
      res[curKey] = cloneDeep(target[curKey], hash)
      return res
      // 初始化参数,假如迭代的对象非最外层级,则以当前对象的原型创建一个新的对象作为下一层递归的初始值
    }, target.constructor === Object ? {} : Object.create(target.constructor.prototype))
  }
}

对数组做一层遍历然后再次递归 验证:

var arr2 = [{a: 1, b: 2, c: 3}, {d: 4, e: 'hey hey'}, {f: false, g: []}]
var copy2 = cloneDeep(arr2)
copy2[0] === arr2[0] // false
var o = arr2[2]
o.f = true
o.g.push('Wow~')
arr2[2] // {f: true, g: Array(1)}
copy2[2] // {f: false, g: Array(0)}