实现一个深拷贝

179 阅读3分钟

概念介绍


首先来认识三个概念:复制、浅拷贝、深拷贝,还是来个简单的总结。

和原始值是否指向同一个对象 第一层数据为基本类型 第一层数据为引用类型
复制 改变会使原始值异同改变 改变会使原始值一同改变
浅拷贝 改变原始值不发生变化 会使原始值一同改变
深拷贝 改变原始值不发生变化 改变原始值不发生变化

我们知道,对象的名字是存在栈内存中的,而它的值是存在堆内存中的。这样的话,复制其实就是讲一个对象的地址指向原始值,名义上和原始值是同一个对象。来一个小小的例子:

let obj1 = {
    name: 'zhangsan',
    age: 35,
    children: ['one', 'two']
}
let obj2 = obj1
obj2.name = 'lisi'
obj2.children.push('three')
console.log(obj1, obj2)   //{name: "lisi", age: 35, children: Array(3)} {name: "lisi", age: 35, children: Array(3)}
打印后发现,obj1和obj2互相影响,不管它的第一层为基本数据还是引用数据,一个发生变化,另外一个也会发生变化。

至于浅拷贝,根据上面表格里的描述,第一层数据为原始值是相互之间不影响,第一层数据为引用值时相互影响,也就是说将origin对象拷贝到target中,但不包括origin中的子对象,我们来手动实现一个浅拷贝:

/**
 * 实现一个浅拷贝
 * @param  origin 原始对象
 * @param  target 拷贝后的对象
 */
function clone(origin, target){
    let tar = target || {}
    for(let key in origin){
        if(origin.hasOwnProperty(key)){
            tar[key] = origin[key]
        }
    }
    return tar
}

至于深拷贝是什么呢?深拷贝就是讲整个原对象拷贝到一个新的对象中,另开辟一个堆内存放新对象的地址,新旧之间没有任何影响。至于怎么实现深拷贝,不着急,先来一个方法的介绍,等下会用到。


Object.prototype.toString方法

如果需要判断一个数据是什么类型的,你首先想到的方法是什么?typeof? instanceof? typeof 只能实现基本类型的判断,对于引用类型,typeof的结果是一致的,都为'object',这样的话我们并不能判断你的数据是对象还是数组,而且最变态的一点,typeof null 居然等于'object''object'。至于instanceof,是用来判断一个实例是否属于某种类型,并不能确切的帮我们找到它的类型,这个时候Object.prototype.toString就要上场了,它可以帮助检测数据类型。

/**
 * 获取数据类型
 * @param  obj 指定的数据
 */
function getDataType(obj){
    let type = Object.prototype.toString.call(obj)
    // 可以得到类似这样的一个字符串  [object Object]
    let reg = /(\[object\s)(\w+)(\])/g
    return type.replace(reg, '$2')    
}

调用下这个方法,

getDataType([])     //"Array"
getDataType({})      //"Object"
getDataType(Math)     //"Math"
getDataType(new Map())  //"Map"
getDataType(new Set())  //"Set
getDataType(null)      //"Null""
...
香不香,啥数据都能检验

实现一个深拷贝

实现深拷贝最简单的方法是通过JSON.parse(JSON.stringify(要拷贝的对象)),不过这种方式会有以下一些弊端:

  • 对象有函数或undefined,会造成函数和undefined的丢失
  • 对象里有RegExp、Error对象,则序列化的结果将只得到空对象
  • 对象里有NaN、Infinity和-Infinity,则序列化的结果会变成null
let originObj = {
    name: 'test',
    say(){
        console.log('123')
    },
    age: undefined,
    date: new RegExp('\\w+')
}

let targetObj = JSON.parse(JSON.stringify(originObj))
targetObj.sex = 'nv'
console.log(originObj, targetObj)

控制台打印originObj, targetObj,可以看到以上列到的一些情况。

既然如此,自己来实现一个深拷贝

/**
 * 深拷贝
 * @param  origin 原对象
 * @param  target 目标对象
 */
function deepClone(origin, target){
   let target = target || {}
   for(let key in origin){
       if(origin.hasOwnProperty(key)){//只拷贝自有属性
           if(typeof origin[key] !== "object"){
               target[key] = origin[key]
           }else{
               target[key] = getDataType(origin[key]) === "Array" ? [], {}   //借助上一步的这个方法
               deepClone(origin[key], target[key])
           }
       }   
   }
   return target
}

来测试一下,原对象和拷贝后的对象互不影响,这样我们就实现了一个深拷贝。