我有一个对象 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
类。我认为没有一个万无一失的通用解决方案来解决这个问题,但我愿意承认我错了!
当我需要实现通用深度复制时,我最终妥协了,假设我只需要复制一个简单的Object
、Array
、Date
、String
、Number
或Boolean
。最后3种类型是不可变的,所以我可以进行浅拷贝而不必担心它发生变化。我还假设包含在Object
或Array
中的所有元素也将是这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对象,但只要你不认为它会为你抛出的任何事情而工作,它可能就足够了。