如何正确地克隆一个JavaScript对象?

发布时间 2023-10-24 19:44:39作者: 小满独家

内容来自 DOC https://q.houxu6.top/?s=如何正确地克隆一个JavaScript对象?

我有一个对象 x。我想要复制它为对象 y,以便对 y 的更改不会修改 x。我意识到,复制从内置JavaScript对象派生的对象将导致额外的、不需要的属性。这不是一个问题,因为我正在复制我自己字面构建的对象之一。

如何正确地克隆一个JavaScript对象?


2022年更新

现在有一个名为结构化克隆的新的JavaScript标准。它在许多浏览器中都可以工作(参见Can I Use)。

const clone = structuredClone(object);

旧答案

要在JavaScript中为任何对象执行此操作并不简单或直接。你会遇到错误地从对象的原型中获取属性的问题,这些属性应该留在原型中,而不是复制到新实例中。例如,如果你像一些答案中描述的那样,将clone方法添加到Object.prototype上,你需要显式地跳过该属性。但是,如果还有其他添加到Object.prototype或其他中间原型的方法,你不知道呢?在这种情况下,你会复制不应该有的属性,所以你需要使用hasOwnProperty方法来检测未预见的、非本地的属性。

除了不可枚举的属性外,当你尝试复制具有隐藏属性的对象时,还会遇到更困难的问题。例如,prototype是函数的一个隐藏属性。此外,一个对象的原型是通过__proto__属性引用的,这也是隐藏的,不会被遍历源对象属性的for/in循环复制。我认为__proto__可能是Firefox JavaScript解释器特有的,在其他浏览器中可能有所不同,但你明白了。不是所有东西都是可枚举的。如果你知道属性的名称,你可以复制隐藏的属性,但我不知道有没有办法自动发现它。

在寻求优雅解决方案的过程中,还有一个问题是如何正确设置原型继承。如果你的源对象的原型是Object,那么简单地使用{}创建一个新通用对象就可以工作,但如果源对象的原型是Object的某个后代,那么你将会缺少通过hasOwnProperty过滤器跳过的该原型中的其他成员,或者那些在原型中但不在一开始就可枚举的成员。一个解决方案可能是调用源对象的constructor属性来获取初始副本对象,然后复制属性,但这样你仍然无法获得非枚举属性。例如,以下代码显示了一个Date对象将其数据存储为隐藏成员:

function clone(obj) {
    if (null == obj || "object" != typeof obj) 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();

/\* Executes function after 5 seconds. \*/
setTimeout(function(){
    var d2 = clone(d1);
    alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
}, 5000);

d1的日期字符串将比d2晚5秒。使一个Date与另一个相同的方法是通过调用setTime方法,但这仅限于Date类。我认为没有一个万无一失的通用解决方案来解决这个问题,但我愿意承认我错了!

当我需要实现通用深度复制时,我最终妥协了,假设我只需要复制一个简单的ObjectArrayDateStringNumberBoolean。最后3种类型是不可变的,所以我可以进行浅拷贝而不必担心它发生变化。我还假设包含在ObjectArray中的所有元素也将是这6个简单类型列表中的一个。这可以通过类似于以下代码来实现:

function clone(obj) {
    var copy;

    // 处理3种简单类型,以及null或undefined
    if (null == obj || "object" != typeof obj) return obj;

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

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

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

    throw new Error("无法复制对象!它不支持的类型。");
}

上述函数将适用于我提到的6种简单类型,只要对象和数组中的数据形成树形结构。也就是说,在对象中没有对同一数据的多个引用。例如:

// 这将是可克隆的:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// 这将可以工作,但你会获得2个内部节点的副本,而不是2个对同一副本的引用
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// 克隆这个会导致栈溢出,因为无限递归:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

它将无法处理任何JavaScript对象,但只要你不认为它会为你抛出的任何事情而工作,它可能就足够了。