前端面试前端文章翻译

JS中的深拷贝,来自stackoverflow的高票答案

2016-08-30  本文已影响173人  mervynyang

深拷贝JS中的任何对象都不是一件容易的事情,你将会遇到这样的问题,从object的原型中,错误的选择应该留在原型上而不是拷贝到新实例上的属性。举个例子,你添加了一个clone的方法到Object.prototype,正如其他答案描述的,你需要显式地跳过该属性。但是如果其他额外的方法或者中间原型也添加到了Object.prototype,而你不知道。在这种情况下,你将会拷贝你不需要的属性,所以你需要用hasOwnProperty方法检查不可预见的、非局部的属性。

除了不可枚举的属性之外,你将会遇到更难的一个问题,就是当你拷贝一个拥有隐藏属性的对象。举个例子,函数的prototype是隐藏的,对象原型的引用__proto__属性也是隐藏的,通过for/in的迭代方法将不能拷贝源对象上的这些属性。我认为Firefox的JS解释器的__proto__属性是比较特殊的,可能跟其他的浏览器有点不同,但是你可以想到,不是一切都是可枚举的。如果你知道属性的名字,那你就能够拷贝这个属性,但是我不知道怎么去自动发现这些隐藏的属性。

另一个障碍是寻找一个优雅的解决方案,正确的设置原型的继承。如果你的源对象是一个Object,那么简单的用{}创建一个普通的对象也会工作。但是如果源对象上的原型是某些Object的后代,那么你使用hasOwnProperty方法过滤的时候,或者在原型链开始的地方是不能枚举的,将会跳过原型,丢失一些额外的成员。一个解决方法是调用源对象上的constructor的属性得到初始化的拷贝对象,然后拷贝属性,不过你仍然不能得到不可枚举的属性。例如,一个Date对象存储的数据是隐藏的。

    function clone(obj) {
        if (obj === null || typeof obj !== 'object') {
            return obj;
        }

        var copy = obj.constructor();

        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) {
                copy[attr] = obj[attr];
            }
        }

        return copy;
    }

    var d1 = new Date();

    // 等五秒钟
    var start = (new Date()).getTime();
    while ((new Date().getTime()) - start < 5000);

    var d2 = clone(d1);

    console.log('d1=' + d1.toString() + 'd2=' + d2.toString());
    // d1=Tue Aug 30 2016 10:19:59 GMT+0800 (CST)d2=Tue Aug 30 2016 10:20:04 GMT+0800 (CST)

d2的值将会比d1的值大5秒钟。有一个让Date类型与另一个Date类型相同的办法,就是调用setTime方法,但是这只是对Date类而言的。我认为这不是一个万无一失的方法,我很乐意是错的!

当我必须实现一个通用的深拷贝的方法时,我最终还是妥协了,假设我只有纯Object、Array、Date、String、Number或者Boolean类型需要拷贝。最后3个类型是不可变的,所以我能够执行浅拷贝而不用担心它改变了。我进一步假设任何包含对象或数组的元素也将是这6个简单类型其中的一个,这可以用下面的代码来实现:

    function clone(obj) {
        var copy;
        
        // 处理3个简单的类型, null 或者 undefined
        if (obj === null || typeof obj !== 'object') {
            return obj;
        }

        if (obj instanceof Date) {
            copy = new Date();
            copy.setTime(obj.getTime());
            return copy;
        }

        if (obj instanceof Array) {
            var copy = [];
            for (var i = 0, len = obj.length; i < len; i++) {
                copy[i] = clone(obj[i]);
            }
            return copy;
        }

        if (obj instanceof Object) {
            var copy = {};
            for (var attr in obj) {
                if (obj.hasOwnProperty(attr)) {
                    copy[attr] = clone(obj[attr]);
                }
            }
            return copy;
        }

        throw new Error("Unable to copy obj! Its type isn't supported.");
    }

上面的方法完全能够在我提到的那6个简单类型中工作,只要对象和数组中的数据形成一个树状结构,也就是说,在1个对象中没有多于1个的对相同数据的引用。例如:

    // 这是可以克隆的
    var tree = {
        "left": { "left": null, "right": null, "data": 3 },
        "right": null,
        "data": 8
    };
    
    // 这样也可以工作,但是你会得到2份内部节点,而不是2个引用相同的副本
    var directedAcyclicGraph = {
        "left"  : { "left" : null, "right" : null, "data" : 3 },
        "data"  : 8
    };

    directedAcyclicGraph["right"] = directedAcyclicGraph["left"];
    
    // 这种情况因为无限的递归,会导致堆栈溢出
    var cylicGraph = {
        "left"  : { "left" : null, "right" : null, "data" : 3 },
        "data"  : 8
    };
    cylicGraph["right"] = cylicGraph;

这个clone的方法不能处理所有的JS对象,但已经能满足大部分的需求了,只要你不把所有的工作的丢给它就可以了。

原文地址:How do I correctly clone a JavaScript object?

上一篇下一篇

猜你喜欢

热点阅读