JavaScript 浅复制与深复制

JavaScript 浅复制与深复制

1 赋值

在JavaScript中,基本数据类型存在栈中,对象作为引用数据类型存在堆中。
JavaScript赋值,对于栈中基本数据类型的赋值,操作的是栈,传递的是值;对于堆中对象的赋值,操作的是栈,传递的是引用。
以下主要讨论堆中的赋值。
如 obj = obj1; 那么obj和obj1指向同一块内存地址,修改obj或obj1的属性,都会引起内存地址存储内容的修改。

var obj = {a:{b:1}};
var obj1 = obj;
// obj1.a.b = 2;
obj.a.b = 2;
console.log(obj.a.b,obj1.a.b);//2 2

赋值的缺陷在于源对象和赋值对象指向相同的内存地址,联系过于紧密。一旦一方修改,另一方也会随之修改。
克隆很好解决了这个问题,克隆对象和被克隆对象,分别存在于两个不同的内存地址之中。

2 浅克隆

上面谈到,克隆和赋值的最大区别在于,赋值双方指向同一个内存地址,而克隆双方存储在不同的内存地址之中。
克隆又分为浅克隆和深克隆。浅克隆和深克隆主要的差异在于

  • 不同点:对于深层属性,浅克隆存在与对源对象的引用关系,而深克隆与源对象之间完全没有引用关系。
  • 相同点:相对于赋值来说,克隆和被克隆对象存储在不同内存地址,赋值双方存储在相同内存地址。
    下面介绍浅克隆的常见方式。

2.1 for-in 浅克隆 只能克隆对象第一层的可枚举属性

广度遍历,不做赘述

2.2 cloneNode(false) 浅克隆

cloneNode的作用对象是DOM结点,当参数为false表示浅克隆,为true表示深克隆
cloneNode(false)浅克隆不能克隆子结点,只能进行一层克隆
这种方式存在的问题是:

  • 针对结点,无法操作非节点对象
  • 不能克隆被addEventListener或onlcick这些用js绑定的事件
  • 就算是cloneNode(true),也只能克隆DOM子树,不存在与祖先节点的链接关系。
  • 以上两点参考MDN
    //cloneNode浅克隆
    var node = document.querySelector("#aaa");
    node.obj = {
        a: {
            b: 2
        }
    }
    var cNode = node.cloneNode(false);
    document.body.appendChild(cNode);
    console.log(node,cNode); 
    console.log(node.childNodes.length,cNode.childNodes.length);//3 0

2.3 Object.assign() 浅克隆

Object.assign()用于将所有可枚举属性的值从一个或多个源对象复制到目标对象
对于Object.assign()来说, 它拷贝的是属性值。假如源对象的属性值是另一个对象的引用,那么它也只指向那个引用。

    let source = { 'name':'prop',obj:{'name':'objprop',innerObj:{num:1}} };
    let clone = Object.assign({}, source);
    source.name = 'prop1';
    source.obj.name="objprop1";
    source.obj.innerObj.num=99;  //改变克隆目标对象的对象属性,克隆对象也随之改变,说明存在引用关系
    console.log('srcObj', clone); 

2.4 解构 浅克隆

解构是ES6中对赋值运算符的一种拓展,可以很方便地在复杂对象中提取数据字段.
解构可以克隆对象深层属性,然而需要注意的是,对于复杂类型,解构克隆的依然是引用。

    var source = {
        a: 1,
        b: function () { },
        c: {
            d: {
                num: 2,
            }
        }
    }
    var clone = { ...source};
    source.c.d.num = 6;//改变source,clone也随之改变
    console.log('clone', clone);

3 深克隆

3.1 JSON序列化实现深克隆及其缺陷 JSON.parse(JSON.stringify(obj))

序列化期间,存在将对象状态写入到临时或持久性存储区的过程,此后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
因此JSON.parse(JSON.stringify(obj))转换之后不存在引用关系。

    var source = {
        a: 1,
        c: {
            d: {
                num: 2,
            }
        }
    }
    var clone = JSON.parse(JSON.stringify(source));
    source.c.d.num = 6;
    console.log('clone', clone);//clone.c.d.num依然为2,说明不存在引用关系,深层属性也完全被拷贝,看似不存在问题。

这种方法表面上实现了深复制,然而在改变测试用例时,结果却大相径庭。
可以发现函数与symbol被忽略,而正则对象变成了空对象{},日期对象也变成了字符串,没有被还原。
JSON.stringfiy()的问题:
遇到对象中undefined、symbol、函数会自动忽略,跳过他们进行序列化
遇到Date对象会转成字符串,正则对象变为空对象。
非数组对象的属性不能保证以特定顺序出现在序列化后的字符串中

    var source = {
            c: () => console.log('clone'),
            d: /clone/,
            e: Symbol(''),
            f: new Date()
    }

    console.log(JSON.parse(JSON.stringify(source)));

对于循环引用,会抛出异常

    //循环引用,父属性和子属性指向同一个对象
    let obj1 = {};
    var c = {a:1}
    obj1.b = c;
    obj1.b.c = c;
    let obj2 = (JSON.parse(JSON.stringify(obj1)));//Converting circular structure to JSON

问题的根源在于JSON.stringify的机制。对此,可参考MDN对SON.stingify()序列化的限制做出的描述说明

3.2 关于实现深克隆的思考

最近逛论坛也看到过很多实现深克隆的方法,但有些方法存在一些问题。
既然是克隆,免不了对属性对象做广度和深度的遍历。
常见的广度遍历的方式有for-in,Object.keys(),Object.assign()等等,常见的深度遍历手法是递归。
然而不少案例在进行广度遍历时,欠缺对属性可枚举性enumerable的考虑,导致克隆方法存在显著缺陷。
举个栗子:

    function deepClone1(obj) {
        var objClone = Array.isArray(obj) ? [] : {};
        if (obj && typeof obj === "object") {
            for (key in obj) {
                if (obj.hasOwnProperty(key)) {
                    if (obj[key] && typeof obj[key] === "object") {
                        objClone[key] = deepClone1(obj[key]);
                    } else {
                        objClone[key] = obj[key];
                    }
                }
            }
        }
        return objClone;
    }

以上方法使用for-in进行广度遍历,而for-in是无法对非枚举属性进行访问的。
对于正则对象的source属性和flag属性这些不可枚举的属性,采用for-in、Object.assign()、Object.keys()等广度遍历方式均无法访问,从而造成克隆的失败。

3.3 实现深克隆

此方式是对3.1、3.2所列举方式的改进,对一些特殊的对象如正则、日期、函数进行了具体处理。

思路:

/* 
    对象的深复制:
    1 初始化返回的目标对象
        如果没有指定返回目标对象,则利用源对象的构造函数创建目标对象,判断源对象类型,正则对象和日期对象分开复制
            如果源对象是正则对象,抽取源对象属性source和flag放入构造参数中新建正则对象
            如果源对象是日期对象,抽取源对象放入构造参数中新建日期对象
        如果指定了目标对象,进入2
    2 复制属性并返回结果
        首先用getOwnProperty(此方法可获取对象的可枚举不不可枚举属性)获取源对象的一级属性名数组props[],遍历此数组获取属性名对应的属性描述对象desc。
        使用typeof 把对象和函数分开处理。
        如果是对象:
            将源对象的属性描述对象设置到对应的目标对象属性中
            考虑到对象存在深度关系,设置对象属性的操作用递归处理
        如果是函数:
            正则提取参数列表和函数体,使用构造创建函数的形式,将二者作为参数传进去
            设置value为fn,其他三个描述属性参照描述属性对象
        如果是字面量
            直接将源属性对应的属性名和属性描述设置给目标属性
*/

实现:

    function deepClone(source, clone) {
        //是否存在源对象
        if (!source) return {};
        //是否指定返回对象
        if (!clone) {
            //正则和日期对象分别处理
            switch (source.constructor) {
                case RegExp:
                    clone = new RegExp(source.source, source.flag);
                    break;
                case Date:
                    clone = new Date(source);
                    break;
                default:
                    clone = new source.constructor();
                    break;
            }
        }
        //获取对象一级属性名数组
        let props = Object.getOwnPropertyNames(source);
        for (let i = 0; i < props.length; i++) {
            //获取属性描述对象
            let desc = Object.getOwnPropertyDescriptor(source, props[i]);
            if (typeof desc.value === "object") {
                //递归
                let o = deepClone(desc.value);
                //设置目标对象属性
                Object.defineProperty(clone, props[i], {
                    configurable: desc.configurable,
                    writable: desc.writable,
                    enumerable: desc.enumerable,
                    value: o
                })
            }
            else if (typeof desc.value === "function") {
                let fnStr = desc.value.toString().replace(/\n/g, "");
                //非贪婪匹配,获取参数列表
                let arg = fnStr.match(/\((.*?)\)/)[1];
                //贪婪匹配,获取函数体
                let content = fnStr.match(/{(.*)}/)[1];
                let fn = new Function(arg, content);
                //设置函数名
                Object.defineProperty(fn, "name", {
                    writable: true,
                    value: desc.value.name
                });
                //设置函数属性
                Object.defineProperty(clone, props[i], {
                    configurable: desc.configurable,
                    enumerable: desc.enumerable,
                    writable: desc.writable,
                    value: fn
                });
            }
            else {
                // 属性值是字面量
                Object.defineProperty(clone, props[i], desc);
            }
        }
        return clone;
    }

测试用例

    var obj = {
        a: 1,
        b: "a",
        c: true,
        d: function (a, b) {
            if (a > 10) a = 10;
            if (b > 5) {
                b = 5;
            } else if (b < 0) {
                b = 0;
            }
            console.log(a + b);
        },
        e: [1, 2, 3, 4],
        f: {
            g: ["a", "b", "c"],
            h: new Date(),
            i: /^[a-z]+$/g,
            j: {
                k: {

                },
                l: [true, false],
                m: [
                    { id: 1001, name: "abc1" },
                    { id: 1002, name: "abc2" },
                    { id: 1003, name: "abc3" }
                ]
            }
        }
    }
    Object.defineProperties(obj.f.j, {
        n: {
            value: function () {
                console.log("abcd");
            }
        },
        o: {
            value: 10,
            enumerable: true
        },
        p: {
            value: [1, 2, 3],
            writable: true
        },
        q: {
            value: true,
            writable: true,
            enumerable: true
        }
    });


    Object.defineProperties(obj.f.j.k, {
        r: {
            value: function () {

            },
            writable: true
        },
        s: {
            value: { a: 1 },
            enumerable: true
        }
    });

    var obj1 = deepClone(obj);

    obj.f.j.p[2] = 10;//obj改变,obj1不改变,不存在引用关系
    console.log(obj, obj1);

效果

至此,方法已经趋于成熟,能对一些常见的对象进行深拷贝,应付日常所需。但是对于一些复杂的情况,如拷贝Vue实例等,可能还存在问题。
这里推荐一个更强大的第三方库lodash作为参考 loadash cloneDeep

posted @ 2020-04-01 23:29  IslandZzzz  阅读(382)  评论(0编辑  收藏  举报