logo
Published on

如何正确地克隆JavaScript对象?

Authors
  • Name
    Twitter

在对 JavaScript 对象进行深度拷贝时,网上的许多解决方案都存在一些问题。因此,我决定写一篇文章来详细讨论这个问题,并说明为什么某些被广泛接受的答案其实并不理想。

起始情况

我想要 深度复制 一个包含各种复杂结构的 JavaScript Object,这个对象可能包含普通属性、循环引用以及嵌套对象。首先,我们定义一个包含循环引用和嵌套对象的结构。

function Circ() {
    this.me = this;
}

function Nested(y) {
    this.y = y;
}

var a = {
    x: 'a',
    circ: new Circ(),
    nested: new Nested('a')
};

接着,我们将 a 复制到变量 b 中,并对其进行修改:

var b = a;

b.x = 'b';
b.nested.y = 'b';

console.log(a, b);

输出结果显示两个对象是同一个引用:

a, b --> 
Object {
    x: "b",
    circ: Circ { me: [Circular] },
    nested: Nested { y: "b" }
}

JSON方案

许多人会尝试使用 JSON 来深度复制:

var b = JSON.parse(JSON.stringify(a));

b.x = 'b';
b.nested.y = 'b';

然而,这样会导致 TypeError: Converting circular structure to JSON 错误,因为 JSON 不支持循环引用。

递归复制(被广泛接受的“答案”)

我们来看一下一个常见的递归复制方法:

function cloneSO(obj) {
    if (null == obj || "object" != typeof obj) return obj;

    if (obj instanceof Date) {
        var 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] = cloneSO(obj[i]);
        }
        return copy;
    }

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

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

var b = cloneSO(a);

b.x = 'b';
b.nested.y = 'b';

然而,这种方法在处理循环引用时会导致 RangeError: Maximum call stack size exceeded 错误。

原生解决方案

经过讨论后,我们发现了一个简单的原生解决方案—— Object.create

var b = Object.create(a);

b.x = 'b';
b.nested.y = 'b';

console.log(a, b);

这种方法虽然能处理循环引用,但在处理嵌套对象时会失效。

原生解决方案的Polyfill

对于较老的浏览器(如IE 8),我们可以使用 Object.create 的 polyfill:

function F() {};
function clonePF(o) {
    F.prototype = o;
    return new F();
}

var b = clonePF(a);

b.x = 'b';
b.nested.y = 'b';

console.log(a, b);

这种方法同样存在与原生解决方案相似的问题,并且生成的对象类型有差异。

更好的(但还不完美)解决方案

我们可以参考 这个问题,并实现一个更好的深度复制方法:

function cloneDR(o) {
    const gdcc = "__getDeepCircularCopy__";
    if (o !== Object(o)) {
        return o; // primitive value
    }

    var set = gdcc in o,
        cache = o[gdcc],
        result;
    if (set && typeof cache == "function") {
        return cache();
    }

    o[gdcc] = function() { return result; };
    result = Array.isArray(o) ? [] : {};
    for (var prop in o) {
        if (prop != gdcc) {
            result[prop] = cloneDR(o[prop]);
        }
    }
    if (set) {
        o[gdcc] = cache;
    } else {
        delete o[gdcc];
    }
    return result;
}

var b = cloneDR(a);

b.x = 'b';
b.nested.y = 'b';

console.log(a, b);

这种方法能真实地深度复制对象,尽管在某些实例的类型上有一些小问题。

结论

最后一种方法使用递归和缓存技术,能够正确处理循环引用和嵌套对象,是一个真正的深度复制方案。尽管它会在某些实例类型上出现问题,但仍然是目前较好的解决方案。

您可以通过这个 jsfiddle 进行进一步测试。