如何在React中进行深度克隆对象?

58
let oldMessages = Object.assign({}, this.state.messages);
// this.state.messages[0].id = 718

console.log(oldMessages[0].id);
// Prints 718

oldMessages[0].id = 123;

console.log(this.state.messages[0].id);
// Prints 123

我如何防止oldMessages成为一个引用,我想改变oldMessages的值而不改变state.messages的值。


3
在我找到更好的替代品之前,MDN提供了一种方法:https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/assign(请参见“//深度克隆”)。 - Tom
1
https://dev59.com/83VD5IYBdhLWcg3wAGiD - Herohtar
this.state.messages 是一个对象还是一个数组? - Jordan Running
1
@JordanRunning 是的 - SpaceDogCS
显示剩余4条评论
4个回答

91

你需要进行深度复制。Lodash的cloneDeep可以轻松实现:

import cloneDeep from 'lodash/cloneDeep';
const oldMessages = cloneDeep(this.state.messages);
oldMessages[0].id = 123;

53

首先,让我们澄清浅克隆和深克隆之间的区别:

浅克隆是将其基本属性克隆,但它的引用属性仍然引用原始对象的克隆。让我解释一下:

let original = {
  foo: "brlja",
  howBigIsUniverse: Infinity,
  mrMethodLookAtMe: () => "they call me mr. Method",
  moo: {
   moo: "MOO"
  }
};

  // shallow copy
  let shallow = Object.assign({}, original);
  console.log(original, shallow); // looks OK

  shallow.moo.moo = "NOT MOO";

  console.log(original, shallow); // changing the copy changed the original

注意如何改变浅复制的非原始属性的内部属性反映在原始对象上。

那为什么我们要使用浅复制?

  • 它肯定更快。
  • 可以通过一行纯JS代码完成。

什么时候会使用浅复制?

  • 你的对象所有属性都是原始值
  • 你正在进行部分复制,其中所有复制的属性都是原始值
  • 您不关心原始对象的命运(是否有理由复制而不是改用该对象?)

好了,让我们开始制作适当的(深度)复制。 深层复制应将原始对象按值复制到克隆体中,并且随着我们深入对象,这种复制应该持续存在。 因此,如果我们在原始属性内部有X级深嵌套对象,则它仍应是副本而不是对内存中相同事物的引用。

大多数人建议滥用JSON API。 他们认为将对象转换为字符串,然后通过它再次转换回对象将产生深层副本。 好吧,是的和不。 让我们试图做到这一点。

扩展我们的原始示例:

  let falseDeep = JSON.parse(JSON.stringify(original));
  falseDeep.moo.moo = "HEY I CAN MOO AGAIN";
  console.log(original, falseDeep); // moo.moo is decoupled

看起来没问题,对吧?错了! 看看从一开始就混入的 mrMethodLookAtMehowBigIsUniverse 属性发生了什么:)

一个返回 null,绝不是 Infinity,另一个则消失了。那可不太好。

简而言之:使用 JSON API 会出现 NaN 或 Infinity 这样“更智能”的值变成 null 的问题。如果您在原始对象的属性中使用了以下内容,则会出现更多问题:方法、正则表达式、映射、集合、Blobs、FileLists、ImageData、稀疏数组、类型化数组。

为什么呢?因为这会产生一些最难以追踪的错误。 我噩梦般地追踪着消失的方法或被转换为其他类型(这通过某人的坏输入参数检查,但然后无法产生有效结果)在 TypeScript 出现之前。

是时候结束了!那正确答案是什么呢?

  • 编写自己的深拷贝实现。我很喜欢你,但请不要在我们有期限要满足的情况下这样做。
  • 使用项目中已经使用的库或框架提供给您的深度克隆函数。
  • Lodash 的 cloneDeep

许多人仍在使用 jQuery。所以在我们的示例中(请将导入放在文件顶部):

import jQ from "jquery"; 
let trueDeep = jQ.extend(true, original, {});
console.log(original, trueDeep);

这个方法是有效的,它可以生成一个漂亮的深拷贝,而且只需要一行代码就能完成。但我们需要导入整个jQuery库。如果项目中已经在使用它,那就没问题了,但我通常会避免使用它,因为它太臃肿而且命名方式非常不一致。

同样地,AngularJS用户可以使用angular.copy()

但如果我的框架或库没有类似的函数呢?

你可以使用我个人最喜欢的JS库之一(我并未参与该项目,只是一个大粉丝)- Lodash(或者用下划线表示)。所以,在我们的例子中添加如下代码(同样要注意import的位置):

import _ from "lodash"; // cool kids know _ is low-dash
var fastAndDeepCopy = _.cloneDeep(objects);
console.log(original, lodashDeep);

这只是一个简单的单行代码,它能够工作,而且速度很快。

基本上就是这样:)

现在你知道了JS中浅层拷贝和深层拷贝之间的区别。你意识到JSON API滥用只是滥用,而不是真正的解决方案。如果你已经在使用jQuery或AngularJS,那么你现在知道已经有了解决方案。如果没有,你可以编写自己的代码,也可以考虑使用lodash库。

完整的示例可以在这里找到: codesandbox - entire example


2
优美的答案,其中一件事是在浏览器上,滥用JSON API后日期似乎没有任何区别(它们都是时间戳),但我理解为什么这是一种滥用,也许将日期示例更改为空类或函数,就像您在最后所说的那样。 - SpaceDogCS
@SpaceDogCS 感谢您的编辑和友善的评论。现在我想起来,"误用"可能是更好的词汇。 "(这都是时间戳)", 看起来取决于我使用的浏览器,在控制台中进行数学计算会有不同的效果。 我可以编辑更多的内容,但我已经列出了一个清单(并加粗了),我的一般意图是解释你不应该这样做,而不是给人留下你应该如何做的印象,同时注意这些陷阱的清单。也许我错过了清单中的某些内容,但那不是重点 :) - DanteTheSmith
1
如果您知道您的层次结构永远不会包含除字符串和整数以外的任何内容,那么使用JSON API似乎是完全可以接受的。编写一个滥用JSON API的抽象函数并重写它并使用更重的深度复制方法并不难。只要分开,如果您需要担心可能会破坏它的扩展,那么就没问题了。此外,我想深度复制方法比JSON解析慢得多,或者我对此错了吗?真正的深度复制需要递归整个结构。JSON呢? - mas
1
我很难理解“花哨的命名方式是面向对象编程中用来吓走辉煌的函数式编程大师的”,所以它的意思是“方法是对象或类内部的函数”。 - rickvian
感谢大家的评论,我已经重新审视了答案,并通过减少代码和文字来展示更多的问题来改进它。此外,我已经删除了关于何时使用浅拷贝的其他选项,因为我认为随着代码库的老化,它会带来更多的麻烦而不是好处。 - DanteTheSmith

11
尝试使用
let tempVar = JSON.parse(JSON.stringify(this.state.statename))

8
虽然这个方法可能可行,但如果您的数据结构无法用JSON表示(例如其中一个键是日期对象),我怀疑会遇到问题。 - RonLugge
1
这很不干净,速度慢,并且如果JSON序列化/反序列化跳过某些部分可能会出现问题。 - mir88
4
请勿使用 JSON API 来实现此目的,这种方法存在许多陷阱。请查看我的答案,了解如何正确地实现。 - DanteTheSmith

3

你实际在做什么

let oldMessages = Object.assign({}, this.state.messages);

浅拷贝类似于使用扩展运算符{...this.state.message}的方式。

对象在内存中有自己的引用,想要销毁它,可以使用JSON.parse(JSON.stringify(object))。无论嵌套的键有多深,它都会删除对象的引用,然后你就会得到一个新的对象。

这个概念被称为深拷贝或深度克隆。


1
不要使用JSON API来深度复制数据(特别是如果你不知道所述数据的外观)。我已经解释了这会导致许多陷阱。 - DanteTheSmith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接