如何正确克隆 JavaScript 对象?

新手上路,请多包涵

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

如何正确克隆 JavaScript 对象?

原文由 soundly_typed 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 707
2 个回答

2022 年更新

有一个称为 结构化克隆 的新 JS 标准。它适用于所有浏览器:

 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() + "\nd2 = " + d2.toString());
 }, 5000);

d1 的日期字符串将比 d2 晚 5 秒。使一个 Date 与另一个 Date 相同的一种方法是调用 setTime 方法,但这是特定于 Date 类的。我认为这个问题没有万无一失的通用解决方案,尽管我很乐意犯错!

当我不得不实现一般的深度复制时,我最终妥协了,假设我只需要复制一个普通的 ObjectArrayDateStringNumberBoolean 。最后 3 种类型是不可变的,所以我可以执行浅拷贝而不用担心它会改变。我进一步假设 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 对象,但它可能足以满足多种用途,只要您不认为它只适用于您扔给它的任何东西。

原文由 A. Levy 发布,翻译遵循 CC BY-SA 4.0 许可协议

如果您不在对象中使用 Date s、函数、undefined、regExp 或 Infinity,一个非常简单的衬里是 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()

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

另请参阅 这篇关于浏览器的结构化克隆算法的文章,该算法 在向工作人员发送消息和从工作人员发送消息时使用。它还包含深度克隆功能。

原文由 heinob 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题