• Javascript 中的深浅拷贝


    工作中经常会遇到需要复制 JS 数据的时候,遇到 bug 时实在令人头疼;面试中也经常会被问到如何实现一个数据的深浅拷贝,但是你对其中的原理清晰吗?一起来看一下吧!

    为什么会有深浅拷贝

    想要更加透彻的理解为什么 JS 会有深浅拷贝,需要先了解下 JS 的数据类型有哪些,一般分为基本类型(Number、String、Null、Undefined、Boolean、Symbol )和引用类型(对象、数组、函数)。

    基本类型是不可变的,任何方法都无法改变一个基本类型的值,也不可以给基本类型添加属性或者方法。但是可以为引用类型添加属性和方法,也可以删除其属性和方法。

    基本类型引用类型在内存中的存储方式也大不相同,基本类型保存在栈内存中,而引用类型保存在堆内存中。为什么要分两种保存方式呢? 因为保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,但是我们可以把它的地址写在栈内存中以供我们访问。

    说来这么多,我们来看个示例:

    
    let num1 = 10;
    let obj1 = {
        name: "hh"
    }
    
    let num2 = num1;
    let obj2 = obj1;
    
    num2 = 20;
    obj2.name = "kk";
    
    console.log(num1); // 10
    console.log(obj1.name); // kk
    

    执行完这段代码,内存空间里是这样的:

    在这里插入图片描述

    可以看到 obj1 和 obj2 都保存了一个指向该对象的指针,所有的操作都是对该引用的操作,所以对 obj2 的修改会影响 obj1。

    小结:

    之所以会出现深浅拷贝,是由于 JS 对基本类型引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象保存在堆内存中的地址,JS 不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。

    在复制时也是一样,如果我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,所以改变其中的一个对象,会影响到另外一个。

    深浅拷贝

    1. 浅拷贝

    浅拷贝只是复制基本类型的数据或者指向某个对象的指针,而不是复制对象本身,源对象和目标对象共享同一块内存;若对目标对象进行修改,存在源对象被篡改的可能。

    我们来看下浅拷贝的实现:

    
    /* sourceObj 表示源对象
     * 执行完函数,返回目标对象
    */
    function shadowClone (sourceObj = {}) {
        let targetObj = Array.isArray(sourceObj) ? [] : {};
        let copy;
        for (var key in sourceObj) {
            copy = sourceObj[key];
            targetObj[key] = copy;
        }
        return targetObj;
    }
    
    
    // 定义 source
    let sourceObj = {
        number: 1,
        string: 'source1',
        boolean: true,
        null: null,
        undefined: undefined,
        arr: [{name: 'arr1'}, 1],
        func: () => 'sourceFunc1',
        obj: {
            string: 'obj1',
            func: () => 'objFunc1'
        }
    }
    
    // 拷贝sourceObj
    let copyObj = shadowClone(sourceObj);
    
    // 修改 sourceObj
    copyObj.number = 2;
    copyObj.string = 'source2';
    copyObj.boolean = false;
    copyObj.arr[0].name = 'arr2';
    copyObj.func = () => 'sourceFunc2';
    copyObj.obj.string = 'obj2';
    copyObj.obj.func = () => 'objFunc2';
    
    // 执行
    console.log(sourceObj);
    /* {
        number: 1,
        string: 'source1',
        boolean: true,
        null: null,
        undefined: undefined,
        arr: [{name: 'arr2'}],
        func: () => 'sourceFunc1',
        obj: {
            func: () => 'objFunc2',
            string: 'obj2'
        }
    }
    */
    
    

    2. 深拷贝

    深拷贝能够实现真正意义上的对象的拷贝,实现方法就是递归调用“浅拷贝”。深拷贝会创造一个一模一样的对象,其内容地址是自助分配的,拷贝结束之后,内存中的值是完全相同的,但是内存地址是不一样的,目标对象跟源对象不共享内存,修改任何一方的值,不会对另外一方造成影响。

    
    /* sourceObj 表示源对象
     * 执行完函数,返回目标对象
    */
    function deepClone (sourceObj = {}) {
        let targetObj = Array.isArray(sourceObj) ? [] : {};
        let copy;
        for (var key in sourceObj) {
            copy = sourceObj[key];
            if (typeof(copy) === 'object') {
                if (copy instanceof Object) {
                    targetObj[key] = deepClone(copy);
                } else {
                    targetObj[key] = copy;
                } 
            } else if (typeof(copy) === 'function') {
                targetObj[key] = eval(copy.toString());
            } else {
                targetObj[key] = copy;
            }
        }
        return targetObj;
    }
    
    
    // 定义 sourceObj
    let sourceObj = {
        number: 1,
        string: 'source1',
        boolean: true,
        null: null,
        undefined: undefined,
        arr: [{name: 'arr1'}],
        func: () => 'sourceFunc1',
        obj: {
            string: 'obj1',
            func: () => 'objFunc1'
        }
    }
    
    // 拷贝sourceObj
    let copyObj = deepClone(sourceObj);
    
    // 修改 source
    copyObj.number = 2;
    copyObj.string = 'source2';
    copyObj.boolean = false;
    copyObj.arr[0].name = 'arr2';
    copyObj.func = () => 'sourceFunc2';
    copyObj.obj.string = 'obj2';
    copyObj.obj.func = () => 'objFunc2';
    
    // 执行
    console.log(sourceObj);
    /* {
        number: 1,
        string: 'source1',
        boolean: true,
        null: null,
        undefined: undefined,
        arr: [{name: 'arr1'}],
        func: () => 'sourceFunc1',
        obj: {
            func: () => 'objFunc1',
            string: 'obj1'
        }
    }
    */
    

    两个方法可以合并在一起:

    
    /* deep 为 true 表示深复制,为 false 表示浅复制
     * sourceObj 表示源对象
     * 执行完函数,返回目标对象
    */ 
    function clone (deep = true, sourceObj = {}) {
        let targetObj = Array.isArray(sourceObj) ? [] : {};
        let copy;
        for (var key in sourceObj) {
            copy = sourceObj[key];
            if (deep && typeof(copy) === 'object') {
                if (copy instanceof Object) {
                    targetObj[key] = clone(deep, copy);
                } else {
                    targetObj[key] = copy;
                } 
            } else if (deep && typeof(copy) === 'function') {
                targetObj[key] = eval(copy.toString());
            } else {
                targetObj[key] = copy;
            }
        }
        return targetObj;
    }
    

    技巧

    1. 浅拷贝技巧

    (1)可以使用 concat() 和 slice() 方法来实现数组的浅拷贝:

    
    let a = [1, {name: 'hh1'}];
    let b = [2, {name: 'kk1'}];
    let copy = a.concat(b);
    copy[1].name = 'hh2';
    copy[3].name = 'kk2';
    console.log(copy);
    // [1, {name: 'hh2'}, 2, {name: 'kk2'}]
    
    

    无论 a[1].name 或者 b[1].name 改变,copy[1].name 的值都会改变。

    
    let a = [1, {name: 'hh1'}];
    let copy = a.slice();
    copy[1].name = 'hh2';
    console.log(a);
    // [1, {name: 'hh2'}]
    

    改变了 a[1].name 后,copy[1].name 的值也改变了。


    (2)可以使用 Object.assign() 来实现对象的浅拷贝:

    Object.assign() 是 ES6 的新函数。Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。Object.assign() 拷贝的是对象的属性的引用,而不是对象本身。

    
    let sourceObj = {
        str: 'hh',
        number: 10,
        obj: {
            str: 'kk1'
        }
    }
    let targetObj = Object.assign({}, sourceObj)
    targetObj.obj.str = 'kk2'
    console.log(sourceObj);
    // {
    //     str: 'hh',
    //     number: 10,
    //     obj: {
    //         str: 'kk2'
    //     }
    // }
    

    修改了 targetObj.obj.str 的值之后,sourceObj.obj.str 的值也改变了。

    2. 深拷贝技巧

    (1)转成 JSON 再转回来:

    用 JSON.stringify() 把对象转成字符串,再用 JSON.parse() 把字符串转成新的对象。

    
    let source = ['hh', 1, [2, 3], {name: 'kk1'}];
    let copy = JSON.parse(JSON.stringify(source));
    copy[2][1] = 4;
    copy[3].name = 'kk2';
    console.log(source);
    // ['hh', 1, [2, 3], {name: 'kk1'}]
    

    可以看出,虽然改变了 copy[2].name 的值,但是 source[2].name 的值没有改变。

    JSON.parse(JSON.stringify(obj)) 不仅能复制数组还可以复制对象,但是几个弊端:
    1)它会抛弃对象的 constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object;
    2)这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp 对象是无法通过这种方式深拷贝。
    3)只有可以转成 JSON 格式的对象才可以这样用,像 function 没办法转成 JSON。

    (2)使用Object.create()方法:

    直接使用let targetObj = Object.create(sourceObj),可以达到深拷贝的效果。

    
    let sourceObj = {name: 'hh1'};
    let targetObj = Object.create(sourceObj);
    targetObj.name = 'hh2';
    console.log(sourceObj); 
    // {name: 'hh1'}
    

    修改 targetObj.name 的值后,sourceObj.name 的值没有跟着变化。

    3. 可以使用的库

    jQuery

    具体使用可以参考:官方文档

    Lodash

    具体使用可以参考:官方文档

    来源:https://segmentfault.com/a/1190000017469386

  • 相关阅读:
    CentOS7 安装 RabbitMQ
    测试工程师 - 要了解的技能总结
    STF 连接其它操作系统上的安卓设备实操介绍【转】
    adb -a server nodaemon,设备一直显示 offline,而 adb devices 一直显示 device【已解决】
    Mac 之 STF 搭建(淘宝源安装)
    无损压缩图片
    jenkins 之 Android 打包及上传至蒲公英
    JoinPoint
    元数据库 information_schema.tables
    @RestControllerAdvice全局异常统一处理
  • 原文地址:https://www.cnblogs.com/datiangou/p/10156099.html
Copyright © 2020-2023  润新知