一步一步实现深拷贝

228 阅读6分钟
原文链接: github.com

简介

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

深拷贝实现

低配版

平常用的最多

JSON.parse(JSON.stringify())

非常简单,非常好用,可以应对大部分业务需求 缺陷: 拷贝其他引用类型、拷贝函数、循环引用等情况

1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式

2.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象

3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失

4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null

5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor

6、如果对象中存在循环引用的情况也无法正确实现深拷贝

基础版

循环遍历

function clone(value) {
  if (typeof value === 'object') {
    let cloneValue = {}
    for (const key in value) {
        cloneValue[key] = value[key]
    }
    return cloneValue
  } else {
    return value
  }
}

创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回 1、如果是原始类型,无需继续拷贝,直接返回 2、如果是引用类型,创建一个新的对象,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上

提升版 1.0

考虑对象的深度,实际上我们并不知道对象有多少层,so可以使用递归解决问题

function clone(value) {
  if (typeof value === 'object') {
    let cloneValue = {}
    for (const key in value) {
        cloneValue[key] = clone(value[key])
    }
    return cloneValue
  } else {
    return value
  }
}

这样,一个比较完善的最基础版1.0深拷贝就完成了 实际上,有待优化,然后下一步

提升版 2.0

考虑对象数组,实际上我们并不知道里面详细数据结构,并不能如我们预想的那样完美 如果是数组对象,递归遍历返回便是数组,so,初始化一个新的数据储存就要经过判断了

function clone(value) {
  if (typeof value === 'object') {
    let cloneValue = Array.isArray(itevaluem) ? [] : {}
    for (const key in value) {
        cloneValue[key] = clone(value[key])
    }
    return cloneValue
  } else {
    return value
  }
}

经过判断,就可以把里面复杂的数据结构给拷贝过来 延伸(判断数组的方法) 1、instanceof Array

let a = []
a instanceof Array // true

let b = {}
b.instanceof Array // false

2、Array.isArray() Array.isArray() 是 ES5 新增用来判断是否是数组的方法

let arr = []
Array.isArray(arr) // true

3、Object.prototype.toString.call()

let arr = []
Object.prototype.toString.call(arr) == "[object Array]"

4、typeof arr ==='object' && arr.length 错误案例

提升版 2.1

上面的方法已经相对比较完善,但是,不排除出现意外,比如某些特殊情况 循环引用

const object = {
    field1: 1,
    field2: undefined,
    field3: {
      child: 'child'
    },
    field4: [2, 4, 8]
}
object.object = object

结果: 最终进入死循环导致栈内存溢出了 原因就是上面的对象存在循环引用的情况,即对象的属性间接或直接的引用了自身的情况 怎么解决?

lodash解决循环引用方法

stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
  return stacked
}
stack.set(value, result)

解决循环引用问题,额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。 这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

检查map中有无克隆过的对象 有 - 直接返回 没有 - 将当前对象作为key,克隆对象作为value进行存储 继续克隆

function clone(value, map = new Map()) {
  if (typeof value === 'object') {
    let cloneValue = Array.isArray(value) ? [] : {}
    if (map.get(value)) {
        return map.get(value)
    }
    map.set(value, cloneValue)
    for (const key in value) {
        cloneValue[key] = clone(value[key], map)
    }
    return cloneValue
  } else {
    return value
  }
}

提升版 2.2

使用 WeakMap 替代Map 简介:WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。 弱引用:在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。 我们默认创建一个对象:const obj = {},就默认创建了一个强引用的对象,我们只有手动将obj = null,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。

function clone(value, map = new WeakMap()) {
  if (typeof value === 'object') {
    let cloneValue = Array.isArray(value) ? [] : {}
    if (map.get(value)) {
        return map.get(value)
    }
    map.set(value, cloneValue)
    for (const key in value) {
        cloneValue[key] = clone(value[key], map)
    }
    return cloneValue
  } else {
    return value
  }
}

提升版 3.0

上面的版本只是考虑了普通的object和array 两种数据类型,实际上还存在其他不常用,甚至不会用到copy方法

判断引用数据类型
function isObject(target) {
  const type = typeof target
  return target !== null && (type === 'object' || type === 'function')
}
获取数据类型
function getType(target) {
  return Object.prototype.toString.call(target)
}

类型列表

const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'

const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const regexpTag = '[object RegExp]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'

不同类型的克隆-1

// 克隆原始类型
if (!isObject(target)) {
  return target
}

// 初始化
const type = getType(target)
let cloneTarget
if (deepTag.includes(type)) {
  cloneTarget = getInit(target, type)
}

// 克隆set
if (type === setTag) {
  target.forEach(value => {
    cloneTarget.add(clone(value,map))
  })
  return cloneTarget
}

// 克隆map
if (type === mapTag) {
  target.forEach((value, key) => {
    cloneTarget.set(key, clone(value,map))
  })
  return cloneTarget
}
不同类型的克隆-2

Bool、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe)
        case regexpTag:
            return cloneReg(targe)
        case symbolTag:
            return cloneSymbol(targe)
        default:
            return null
    }
}
克隆Symbol和正则类型
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe))
}

克隆正则:

function cloneReg(targe) {
    const reFlags = /\w*$/
    const result = new targe.constructor(targe.source, reFlags.exec(targe))
    result.lastIndex = targe.lastIndex
    return result
}

完整版代码

deepClone.js

详细可lodash源码学习
https://github.com/lodash/lodash/blob/master/.internal/baseClone.js