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

58 浏览
0 Comments

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

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

如何正确地克隆JavaScript对象?

admin 更改状态以发布 2023年5月20日
0
0 Comments

如果你的对象中没有使用 Date、函数、未定义的值、正则表达式或无限大的值,那么一个非常简单的一行代码就可以实现深拷贝:JSON.parse(JSON.stringify(object))

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

这适用于包含对象、数组、字符串、布尔值和数字的所有类型的对象。

还可以参考关于浏览器的结构化克隆算法的文章,该算法在与工作者之间发送消息时使用。它还包含了一个深度克隆的函数。

0
0 Comments

2022更新

有一个新的JS标准叫做结构化克隆。它在许多浏览器中都可以使用(参见Can I Use)。

const clone = structuredClone(object);

旧答案

在JavaScript中对于任何对象来说,实现这个功能并不简单或直接。你会遇到一个问题,即错误地从对象的原型中获取应该留在原型中而不是复制到新实例的属性。例如,如果你正在给Object.prototype添加一个clone方法,就像某些答案描述的那样,你需要明确地跳过那个属性。但是,如果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() + "
d2 = " + d2.toString());
}, 5000);

d1的日期字符串将比d2晚5秒。一种使一个Date与另一个相同的方法是调用setTime方法,但这只适用于Date类。我认为没有一个完全可靠的通用解决方案来解决这个问题,虽然我很乐意被证明是错的!

当我需要实现通用的深层复制时,我最终妥协了,假设我只需要复制普通的ObjectArrayDateStringNumberBoolean。最后三种类型是不可变的,所以我可以执行浅拷贝而不必担心它们会改变。我进一步假设在ObjectArray中包含的任何元素也将是上述列表中的6种简单类型之一。可以使用以下代码实现:

function clone(obj) {
    var copy;
    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;
    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }
    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }
    // Handle 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("Unable to copy obj! Its type isn't supported.");
}

上述函数对于我提到的6种简单类型将可以正常工作,只要对象和数组中的数据形成树状结构,也就是说,在对象中不存在对同一数据的多个引用。例如:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};
// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];
// Cloning this would cause a stack overflow due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

它无法处理任何JavaScript对象,但对于许多目的来说可能已经足够了,只要你不认为它可以适用于任何你想用它处理的对象。

0