发布于 2026-01-06 0 阅读
0

深度复制和不可篡改性问题

深度复制和不可篡改性问题

在最新一期的《我不知道自己在做什么》中,我发现我之前对 Javascript 中不可变性的所有认知都是错误的。

好吧,我承认我有点夸张了。并非所有事情都是谎言,但我对其中一部分基本概念的理解是错误的。在和几个人讨论过这个问题后,我发现这似乎是大家普遍存在的误解。

这一切都源于我们在 JavaScript 中复制对象方式的一个微妙但根本的区别:深拷贝和浅拷贝。

对于真正不可变的数据,我们需要的是深度复制。它不仅复制对象自身的所有值,还复制对象内所有子对象的所有值。而浅复制则不同,它不仅复制对象自身的所有值,还复制对象内所有子对象的引用。这就是让我困惑的地方。

为了理解这个问题,我们需要了解复制对象的三种方法

参考文献

好的,那我们把步骤简化到最基本的形式。我们创建一个指向对象的可变引用。

const initialObject = { name: "Sam", twitter: "@samdbeckham" };
const newObject = initialObject;

这对不可篡改性不利,因为任何更改都会像这样newObject反映出来:initialObject

newObject.twitter = "@frontendne";
console.log(initialObject.twitter); // @frontendne

在这个例子中,`a`newObject是对 `b` 的引用initialObject。因此,无论何时我们获取或设置这两个对象中的任何一个的数据,该操作也会应用到另一个对象上。这在很多方面都很有用,但不利于实现不可变性。

浅复制

这是以不可变方式复制数据的最常见方法。我们使用扩展运算符来创建副本initialObject。如果您之前使用过 Redux,您应该在 reducer 中见过这种写法。

const initialObject = { name: "Sam", twitter: "@samdbeckham" };
const newObject = { ...initialObject };

这是一个细微的改变,但却...至关重要。newObject它不再与原对象关联initialObject。现在它是数据的副本,是一个全新的对象。因此,如果我们进行之前相同的更改,就会得到以下结果:

newObject.twitter = "@frontendne";
console.log(initialObject.twitter); // @samdbeckham
console.log(newObject.twitter); // @frontendne

修改数据newObject不会initialObject再产生任何影响。我们可以照常进行日常操作,修改后的数据newObject仍然initialObject保持干净。

但这只是浅拷贝,不可变性只存在于一层。为了证明这一点,我们需要在我们的对象内部创建一个对象initialObject

const initialObject = {
  name: "Sam",
  social: {
    twitter: "@samdbeckham",
    youtube: "frontendne"
  }
};
const newObject = { ...initialObject };

乍一看,这newObject似乎是原文件的不可更改副本initialObject,但看看当我们这样做时会发生什么:

newObject.social.twitter = "@frontendne";

console.log(initialObject.social.twitter); // @frontendne

遗憾的是,这种不可变性只是表面现象。一旦我们深入到下一层,就会发现我们又回到了引用值的状态。如果我们展开它newObject,它看起来会像这样:

const newObject = {
  name: "Sam",
  social: initialObject.social
};

我们可以通过向下浅复制一层并newObject这样定义来解决这个问题:

const newObject = {
  ...initialObject,
  social: { ...initialObject.social }
};

在 Redux 中,通常都是这样处理问题的,但这只会增加一层不可变性。如果还有其他嵌套对象,它们仍然会以引用的形式存储。你可以想象(对于某些数据结构而言),这会变得多么混乱。

注意: Object.assign()并且Object.freeze()与 Spread 存在相同的浅拷贝问题。

深度复制

最后,我们来谈谈深度复制。深度复制赋予了对象真正的不可变性。我们可以修改对象中的任何值——无论它嵌套多深——都不会改变我们复制的原始数据。

const initialObject = {
  name: "Sam",
  social: {
    twitter: "@samdbeckham",
    youtube: "frontendne"
  }
};
const newObject = deepCopy(initialObject);

newObject.social.twitter = "@frontendne";

console.log(initialObject.social.twitter); // @samdbeckham
console.log(newObject.social.twitter); // @frontendne

太棒了!我们永不改变!

遗憾的是,JavaScript 没有提供名为 `deepcopy` 的函数deepCopy(),所以我们不得不自己实现;而且实现方式并不优雅。在 JavaScript 中,处理深度复制并没有“优雅”的方法。Das Surma 写了一篇关于深度复制的文章,其中包含一些不错的示例,这里列举一些比较简单的例子。

JSON

这是最简洁易懂的方法,如下所示:

const deepCopy = object => JSON.parse(JSON.stringify(object));

首先,我们使用 `JSON.stringify()` 将对象转换为 JSON 字符串,JSON.stringify()然后再使用该字符串转换回对象JSON.parse()。字符串化数据会丢弃所有引用,使返回的对象完全不可变。但是,如果我们需要在该对象中保留任何引用,它们都会丢失。如果对象中包含任何映射、正则表达式、日期或其他特殊类型,它们也会丢失。如果对象内部存在任何循环引用(这是不应该的),整个过程就会崩溃并抛出错误。因此,这种方法并不健壮。

数据洗钱

如果你不想处理 JSON 解析器带来的问题,可以使用一些方法——虽然这些方法有点取巧。这些方法的核心都是将数据传递给某个服务,然后查询该服务来获取清洗后的数据。这就像洗钱,只不过对象是数据,而且远没有洗钱那么酷。

例如,我们可以使用通知 API:

const deepCopy = object =>
  new Notification("", {
    data: object,
    silent: true
  }).data;

这会触发一条通知,然后将其静音,最后返回该通知中的数据。遗憾的是,用户必须能够接收通知才能使此功能正常工作。

我们也可以用类似的方式利用历史记录 API messageChannel。但它们都有各自的缺点。

现在该怎么办?

深度复制是一种过于强硬的不可变性实现方式。了解浅复制的陷阱应该足以应对大多数问题。你可以使用上面提到的嵌套扩展方法来修复任何问题区域。
如果这种方法开始变得难以驾驭,你应该首先考虑改进数据结构。

如果你确实需要深度复制,别担心。HTML规范中有一个问题试图通过引入`__delete__`来解决这个问题structuredClone()。这个问题越受关注,就越有可能被实现。在此之前,我建议使用像Immutable.js这样的库来处理不可变性。或者,你也可以使用underscorecloneDeep()库中的辅助函数来快速解决这个问题。

如果你想挑战一下,不妨尝试提出你自己的 deepCopy 解决方案。我的朋友 Niall在 Twitter 上分享了一些想法,玩得不亦乐乎。我很想看看你们会提出什么方案。

这篇文章最初发表在我的网站上。

文章来源:https://dev.to/samdbeckham/deep-copy-and-the-immutability-issue--cd6