乐闻世界logo
搜索文章和话题

JS 如何正确克隆一个对象

7 个月前提问
3 个月前修改
浏览次数56

6个答案

1
2
3
4
5
6

当我们在JavaScript中克隆对象时,我们的目的是创建一个新的对象,它具有与原始对象相同的属性和值,但是在内存中占据不同的位置。这意味着,当我们修改新对象时,原始对象不会受到影响,反之亦然。以下是几种常用的克隆JavaScript对象的方法:

浅克隆(Shallow Clone)

Object.assign()

javascript
let original = { a: 1, b: 2 }; let clone = Object.assign({}, original);

这里的Object.assign()方法会将所有可枚举的自有属性从一个或多个源对象复制到目标对象(在这里是一个空对象),然后返回目标对象。

Spread Operator (ES6)

javascript
let original = { a: 1, b: 2 }; let clone = { ...original };

扩展运算符...允许一个表达式在某处被扩展为多个参数(对于函数调用)或多个元素(对于数组字面量)或多个键值对(对于对象字面量)。

以上两种方法都是浅克隆,这意味着如果原始对象的属性值是一个对象,那么克隆对象的这个属性值仅仅是原始对象属性值的引用。如果修改了这个内部对象,原始对象和克隆对象都会受到影响。

深克隆(Deep Clone)

JSON方法

javascript
let original = { a: 1, b: { c: 3 } }; let clone = JSON.parse(JSON.stringify(original));

这种方法非常简单,可以用来深克隆一个对象。但是它有一些限制,例如它不会克隆函数,会忽略undefined,也不能处理循环引用的对象。

递归方式的深克隆

javascript
function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { let arrCopy = []; obj.forEach((v, i) => arrCopy[i] = deepClone(v)); return arrCopy; } if (obj instanceof Object) { let copy = {}; Object.getOwnPropertyNames(obj).forEach(prop => { copy[prop] = deepClone(obj[prop]); }); return copy; } throw new Error('Unable to copy object!'); } let original = { a: 1, b: { c: 3 } }; let clone = deepClone(original);

这个方法会递归地克隆对象,包括其属性值是对象的情况。这样我们得到的克隆对象是完全独立于原始对象的。

库方法

有些JavaScript库(如 Lodash)提供了深克隆的功能。例如,使用Lodash的_.cloneDeep()

javascript
let _ = require('lodash'); let original = { a: 1, b: { c: 3 } }; let clone = _.cloneDeep(original);

这种方式非常简便,不需要自己编写复杂的深克隆逻辑,并且能够处理更复杂的情况,比如循环引用、特殊的对象类型等。

总结起来,选择哪种克隆方式取决于你需要的克隆深度和对象的复杂性。对于简单的情况,浅克隆可能就足够了。当对象结构更复杂,或者需要完全独立副本时,深克隆则是更好的选择。在实际工作中,我们通常倾向于使用成熟的库方法来处理这些事情,以减少bug和提高开发效率。

2024年6月29日 12:07 回复

2022年更新

有一个新的 JS 标准称为结构化克隆。它适用于许多浏览器(请参阅我可以使用)。

shell
const clone = structuredClone(object);

旧答案

要对 JavaScript 中的任何对象执行此操作都不会简单或直接。您将遇到错误地从对象原型中获取属性的问题,这些属性应该保留在原型中而不是复制到新实例中。例如,如果您要添加一个clone方法Object.prototype,正如一些答案所描述的那样,您将需要显式跳过该属性。但是,如果添加了Object.prototype您不知道的其他附加方法或其他中间原型怎么办?在这种情况下,您将复制不应该复制的属性,因此您需要使用该方法检测不可预见的非本地属性hasOwnProperty

除了不可枚举的属性之外,当您尝试复制具有隐藏属性的对象时,您还会遇到更棘手的问题。例如,prototype是函数的隐藏属性。此外,对象的原型是通过 attribute 引用的__proto__,该属性也是隐藏的,并且不会被迭代源对象属性的 for/in 循环复制。我认为__proto__这可能是 Firefox 的 JavaScript 解释器特有的,在其他浏览器中可能有所不同,但你明白了。并非所有事情都是可枚举的。如果您知道隐藏属性的名称,则可以复制它,但我不知道有什么方法可以自动发现它。

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。如果源对象的原型是Object,那么只需创建一个新的通用对象{}即可,但是如果源的原型是 的后代Object,那么您将丢失使用hasOwnProperty过滤器跳过的该原型中的其他成员,或者存在于原型中,但一开始就不可枚举。一种解决方案可能是调用源对象的constructor属性来获取初始复制对象,然后复制属性,但您仍然不会获得不可枚举的属性。例如,Date对象将其数据存储为隐藏成员:

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

当我必须实现一般深度复制时,我最终妥协了,假设我只需要复制普通的Object, Array, Date, String, Number, 或Boolean。最后 3 种类型是不可变的,因此我可以执行浅复制而不用担心它会发生变化。我进一步假设Object或中包含的任何元素Array也将是该列表中的 6 种简单类型之一。这可以通过如下代码来完成:

shell
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 种简单类型。也就是说,对对象中相同数据的引用不超过一次。例如:

shell
// 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 对象,但它可能足以满足许多目的,只要您不认为它只适用于您扔给它的任何东西。

2024年6月29日 12:07 回复

如果您不在Date对象中使用 s、functions、undefined、regExp 或 Infinity,则一个非常简单的行是JSON.parse(JSON.stringify(object))

shell
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()

运行代码片段Hide results

展开片段

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

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

2024年6月29日 12:07 回复

在 ECMAScript 6 中,有一个Object.assign方法,它将所有可枚举自身属性的值从一个对象复制到另一个对象。例如:

shell
var x = {myProp: "value"}; var y = Object.assign({}, x);

但请注意,这是浅复制- 嵌套对象仍被复制为引用。

2024年6月29日 12:07 回复

使用 jQuery,您可以使用扩展进行浅复制

shell
var copiedObject = jQuery.extend({}, originalObject)

的后续更改copiedObject不会影响originalObject,反之亦然。

或者进行深层复制

shell
var copiedObject = jQuery.extend(true, {}, originalObject)
2024年6月29日 12:07 回复

根据MDN

  • 如果您想要浅复制,请使用Object.assign({}, a)
  • 对于“深度”复制,请使用JSON.parse(JSON.stringify(a))

不需要外部库,但您需要先检查浏览器兼容性

2024年6月29日 12:07 回复

你的答案