剖析深浅拷贝、深拷贝的手写实现及原理

900 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情

深浅拷贝的区别

一句土话总结:把a的值赋值给b,改变b的值,a如果跟着变了,那就是浅拷贝,否则就是深拷贝。
看一段代码:

var a=12;
var b=a;
b=15;   
console.log(a,b);//12 15
var obj1={name:'静静'};
var obj2=obj1;
obj2.name='丽颖';  
console.log(obj1.name,obj2.name); //丽颖 丽颖

可以看到基本类型,新变量b改变值,不影响原始变量a的值,因为是按值访问的,因此彼此是独立的互不影响的,所以对于基本类型来说浅拷贝深拷贝的没啥意义;而obj1和obj2属于引用类型,obj2值改变后,obj1也跟着改变了,因为,引用类型是按地址访问的,并且obj1和obj2他们的引用的地址是同一个,所以互相影响。
因为对于基本类型来说浅拷贝深拷贝没啥意义,所以深浅拷贝主要针对的引用类型来说一下 官方点详细的总结:
浅拷贝:只赋值对象的第一级属性值,如果对象的第一级属性中又包含引用类型的属性值,则只复制地址。
浅拷贝的问题:如果对象中又包含引用类型的属性值,则导致克隆后,新旧对象依然共用一个引用类型的属性值(也可以说成地址)。
结果:任意一方修改了引用类型的对象内容,都会导致另一方同时受影响。
深拷贝:不但复制对象的第一级属性值,而且,即使对象中又包含引用类型的属性值,深拷贝也会继续复制内嵌类型的属性值。
结果:克隆后,两个对象彻底再无瓜葛互不影响。

浅拷贝

  • Object.assign(target,sourse)可实现
var obj1={name:'静静',son:{name:'小明'}};
var obj2=Object.assign({},obj1);
obj2.name='丽颖';
obj2['son'].name='小涛'

console.log(obj1,obj2);

看输出:

image.png 只能实现一级的深拷贝,一级的值互不影响,但是一级以上的就不行,比如一级属性值是一个引用类型对象,就依旧会影响原始值,因为复制的是一个地址,原始值对应的属性值和新对象对应的属性值,他们的引用的地址是同一个,依旧互相影响着,所以这也属于浅拷贝。

怎么实现深拷贝

  • JSON.pase(JSON.stringfy())
var obj1={name:'静静'};
var obj2=JSON.parse(JSON.stringify(obj1));
obj2.name='丽颖';
console.log(obj1.name,obj2.name); //静静 丽颖

JSON.stringfy(), 将对象先转成字符串,JSON.parse(),再将JSON字符串转成对象。
但是有个缺点:无法深拷贝undefined值和内嵌函数

加个undefined和内嵌函数试试,如下:

var obj1={name:'静静',son:{name:'小明'},age:undefined,value:()=>console.log(this.name)};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name='哈哈'
console.log(obj1, obj2);

打印如下:

image.png

age和value并没有被拷贝进来。

解决:利用递归

  • 递归 我们自定义一个递归拷贝函数(适用于任何类型的数据)
function deepClone(target) {
    //定义一个变量,准备接新副本对象
    let newObj;
    //如果是一个引用类型
    if (typeof target === 'object') {
        //如果是个数组
        if (Array.isArray(target)) {
            //将新副本赋值为空数组,并遍历
            newObj = []
            for (let item in target) {
                //递归拷贝数组中的每一项
                newObj.push(deepClone(target[item]));
            }
        }
        //判断当前值是null,直接赋值为null
        else if (target === null) {
            newObj = null;
        }
        //判断当前值是一个正则表达式对象,则直接赋值
        else if (target.constructor === RegExp) {
            newObj = target;
        }
        //否则为一个普通的对象,直接for in循环递归遍历复制对象中的每个属性值
        else {
            newObj = {};
            for (var item in target) {
                newObj[item] = deepClone(target[item])
            }
        }
    }
    //如果不是引用类型而是基本类型,那么直接赋值
    else{
      newObj=target;
    }
    //返回最终结果newObj
    return newObj;
}

深拷贝原理
先定义一个变量,去接新副本对象,然后去判断类型,是数组的话就继续判断每个当前值的类型,
最后把每个值复制到新对象里来;如果是null的话,就会直接赋值为为null,
因为typeof null也是object类型,如果类型是RegExp,直接赋值为原始对象target,
以上都不是的话,那就是普通对象,让新副本的属性名跟原始对象target的属性名同名,
新副本的属性值跟原始对象target的属性值相同,
如果target原属性值不是原始数据,会再次进行类型判断,最后赋值。

亲测一下这个方法:

function deepClone(target) {
    //定义一个变量,准备接新副本对象
    let newObj;
    //如果是一个引用类型
    if (typeof target === 'object') {
        //如果是个数组
        if (Array.isArray(target)) {
            //将新副本赋值为空数组,并遍历
            newObj = []
            for (let item in target) {
                //递归拷贝数组中的每一项
                newObj.push(deepClone(target[item]));
            }
        }
        //判断当前值是null,直接赋值为null
        else if (target === null) {
            newObj = null;
        }
        //判断当前值是一个正则表达式对象,则直接赋值
        else if (target.constructor === RegExp) {
            newObj = target;
        }
        //否则为一个普通的对象,直接for in循环递归遍历复制对象中的每个属性值
        else {
            newObj = {};
            for (var item in target) {
                newObj[item] = deepClone(target[item])
            }
        }
    }
    //如果不是引用类型而是基本类型,那么直接赋值
    else{
      newObj=target;
    }
    //返回最终结果newObj
    return newObj;
}
var obj1={name:'静静',son:{name:'小明'}};
var obj2=deepClone(obj1);
obj2.name='丽颖';
obj2['son'].name='小涛'

console.log(obj1,obj2);

输出:

image.png
我们可以看到:
obj2里面的第一级name属性值改变了,并且第一级的属性son它的值是一个引用类型对象,它也改变了,但是,最后最终也没有影响原始变量obj1的值。

我们再试试,undefined和内嵌函数是否能被拷贝进来:

function deepClone(target) {
    //定义一个变量,准备接新副本对象
    let newObj;
    //如果是一个引用类型
    if (typeof target === 'object') {
        //如果是个数组
        if (Array.isArray(target)) {
            //将新副本赋值为空数组,并遍历
            newObj = []
            for (let item in target) {
                //递归拷贝数组中的每一项
                newObj.push(deepClone(target[item]));
            }
        }
        //判断当前值是null,直接赋值为null
        else if (target === null) {
            newObj = null;
        }
        //判断当前值是一个正则表达式对象,则直接赋值
        else if (target.constructor === RegExp) {
            newObj = target;
        }
        //否则为一个普通的对象,直接for in循环递归遍历复制对象中的每个属性值
        else {
            newObj = {};
            for (var item in target) {
                newObj[item] = deepClone(target[item])
            }
        }
    }
    //如果不是引用类型而是基本类型,那么直接赋值
    else{
      newObj=target;
    }
    //返回最终结果newObj
    return newObj;
}
var obj1={name:'静静',son:{name:'小明'},age:undefined,value:()=>console.log(this.name)};
var obj2=deepClone(obj1);
obj2.name='丽颖';
obj2['son'].name='小涛'

console.log(obj1,obj2);

打印如下:

image.png

有被拷贝进来;