为什么不可变性在 JavaScript 中如此重要(或需要)?

IT技术 javascript reactjs functional-programming immutability immutable.js
2021-01-14 22:15:22

我目前正在研究React JSReact Native框架。在半路上,当我阅读 Facebook 的 Flux 和 Redux 实现时,我遇到了 Immutability 或Immutable-JS 库

问题是,为什么不变性如此重要?改变对象有什么问题?这不是让事情变得简单吗?

举个例子,让我们考虑一个简单的新闻阅读器应用程序,它的打开屏幕是新闻标题的列表视图。

如果我设置了一个初始的对象数组,我将无法操作它。这就是不变性原则所说的,对吧?(如果我错了,请纠正我。)但是,如果我有一个新的 News 对象需要更新怎么办?通常情况下,我可以将对象添加到数组中。在这种情况下我如何实现?删除商店并重新创建它?将一个对象添加到数组中不是一个成本更低的操作吗?

6个回答

我最近一直在研究同一主题。我会尽我所能回答你的问题,并尝试分享我到目前为止学到的东西。

问题是,为什么不变性如此重要?改变对象有什么问题?这不是让事情变得简单吗?

基本上它归结为不变性增加了可预测性、性能(间接)并允许突变跟踪的事实。

可预测性

突变隐藏了变化,这会产生(意外的)副作用,这可能会导致令人讨厌的错误。当您强制实施不变性时,您可以使您的应用程序架构和心理模型保持简单,从而更容易对您的应用程序进行推理。

表现

尽管向不可变对象添加值意味着需要在需要复制现有值的地方创建一个新实例,并且需要将新值添加到新对象中,这会消耗内存,但不可变对象可以利用结构共享来减少内存高架。

所有更新都返回新值,但共享内部结构以显着减少内存使用(和 GC 抖动)。这意味着如果你附加到一个有 1000 个元素的向量,它实际上不会创建一个新的 1001 个元素长的向量。最有可能的是,内部只分配了几个小对象。

您可以在此处阅读更多相关信息

突变追踪

除了减少内存使用之外,不变性还允许您通过使用引用和值相等来优化您的应用程序。这使得查看是否有任何更改变得非常容易。例如,react组件中的状态变化。您可以shouldComponentUpdate通过比较状态对象来检查状态是否相同,并防止不必要的渲染。您可以在此处阅读更多相关信息

其他资源:

如果我设置了一个初始值的对象数组。我无法操纵它。这就是不变性原则所说的,对吗?(如果我错了,请纠正我)。但是,如果我有一个新的 News 对象需要更新怎么办?通常情况下,我可以将对象添加到数组中。在这种情况下我如何实现?删除商店并重新创建它?将一个对象添加到数组中不是一个成本更低的操作吗?

是的,这是正确的。如果您对如何在应用程序中实现这一点感到困惑,我建议您查看redux如何做到这一点以熟悉核心概念,它对我帮助很大。

我喜欢以 Redux 为例,因为它包含不变性。它有一个单一的不可变状态树(称为store),其中所有状态更改都是通过分派由 reducer 处理的操作来显式更改的,reducer 接受前一个状态和所述操作(一次一个)并返回应用程序的下一个状态. 您可以在此处阅读有关其核心原则的更多信息

egghead.io有一个很好的 redux 课程,其中redux 的作者Dan Abramov解释了这些原则如下(我稍微修改了代码以更好地适应场景):

import React from 'react';
import ReactDOM from 'react-dom';

// Reducer.
const news = (state=[], action) => {
  switch(action.type) {
    case 'ADD_NEWS_ITEM': {
      return [ ...state, action.newsItem ];
    }
    default: {
        return state;
    }
  }
};

// Store.
const createStore = (reducer) => {
  let state;
  let listeners = [];

  const subscribe = (listener) => {
    listeners.push(listener);

    return () => {
      listeners = listeners.filter(cb => cb !== listener);
    };
  };

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach( cb => cb() );
  };

  dispatch({});

  return { subscribe, getState, dispatch };
};

// Initialize store with reducer.
const store = createStore(news);

// Component.
const News = React.createClass({
  onAddNewsItem() {
    const { newsTitle } = this.refs;

    store.dispatch({
      type: 'ADD_NEWS_ITEM',
      newsItem: { title: newsTitle.value }
    });
  },

  render() {
    const { news } = this.props;

    return (
      <div>
        <input ref="newsTitle" />
        <button onClick={ this.onAddNewsItem }>add</button>
        <ul>
          { news.map( ({ title }) => <li>{ title }</li>) }
        </ul>
      </div>
    );
  }
});

// Handler that will execute when the store dispatches.
const render = () => {
  ReactDOM.render(
    <News news={ store.getState() } />,
    document.getElementById('news')
  );
};

// Entry point.
store.subscribe(render);
render();

此外,这些视频更详细地演示了如何实现以下方面的不变性:

@terabaud 感谢分享链接。我同意这是一个重要的区别。^_^
2021-03-19 22:15:22
@bozzmob 不客气!不,这是不正确的,您需要自己在减速器中强制执行不变性。这意味着您可以遵循视频中演示的策略或使用像 immutablejs 这样的库。您可以在此处此处找到更多信息
2021-03-25 22:15:22
请解释这一点“变异隐藏了变化,这会产生(意外的)副作用,这可能会导致令人讨厌的错误。当你强制执行不变性时,你可以保持你的应用程序架构和心理模型简单,这使得你的应用程序更容易推理。” 因为这在 JavaScript 的上下文中根本不是真的。
2021-03-30 22:15:22
@naomik 感谢您的反馈!我的目的是说明这个概念,并明确表明对象不会发生变异,也不一定要展示如何始终实现它。但是,我的示例可能有点混乱,我稍后会对其进行更新。
2021-04-11 22:15:22
@naomik ES6const与不变性无关Mathias Bynens 写了一篇关于它的很棒的博客文章
2021-04-12 22:15:22

不变性的逆向观点

TL/DR:在 JavaScript 中,不变性与其说是必需品,不如说是一种时尚趋势。如果您正在使用 React,它确实为状态管理中一些令人困惑的设计选择提供了一种巧妙的解决方法然而,在大多数其他情况下,它不会为其引入的复杂性增加足够的value,更多地是为了充实简历而不是满足实际的客户需求。

长答案:阅读下文。

为什么不可变性在 javascript 中如此重要(或需要)?

好吧,我很高兴你问了!

前段时间,一个非常有才华的人Dan Abramov编写了一个名为Redux的 JavaScript 状态管理库,它使用纯函数和不变性。他还制作了一些非常酷的视频,使这个想法非常容易理解(和销售)。

时机恰到好处。Angular的新鲜感正在消退,JavaScript 世界已经准备好关注具有适当酷炫程度的最新事物,而这个库不仅具有创新性,而且与被另一家硅谷巨头兜售的React完美契合

尽管很可悲,但时尚在 JavaScript 世界中占据主导地位。现在阿布拉莫夫被誉为半神,而我们凡人都必须服从不变……这是否有意义。

改变对象有什么问题?

没有什么!

事实上,程序员一直在改变对象……只要有要改变的对象。换句话说,50 多年的应用程序开发经验。

为什么要把事情复杂化?当您有对象cat并且它死了时,您真的需要一秒钟cat来跟踪更改吗?大多数人只会说cat.isDead = true并完成它。

(变异对象)不会让事情变得简单吗?

是的!..当然可以!

特别是在 JavaScript 中,它在实践中最有用的是呈现在其他地方(例如在数据库中)维护的某些状态的视图。

如果我有一个新的 News 对象需要更新怎么办?...在这种情况下我如何实现?删除商店并重新创建它?将一个对象添加到数组中不是一个成本更低的操作吗?

好吧,您可以采用传统方法并更新News对象,因此对象的内存表示会发生变化(以及向用户显示的视图,或者人们希望如此)......

或者替代...

您可以尝试性感的 FP/Immutability 方法,并将您对News对象的更改添加到跟踪每个历史更改的数组中,这样您就可以遍历该数组并找出正确的状态表示应该是什么(呸!)。

我正在尝试了解这里的内容。请赐教我:)

时尚来来去去,伙计。有很多方法可以给猫剥皮。

很抱歉,您不得不忍受一组不断变化的编程范式的混乱。但是,嘿,欢迎来到俱乐部!!

现在有几个重要的点需要记住,关于不变性,你会以只有天真才能聚集的狂热强度向你抛出这些。

1) 不变性对于避免多线程环境中的竞争条件非常有用

当多个线程想要更​​改对象时,多线程环境(如 C++、Java 和 C#)会犯锁定对象的做法。这对性能不利,但比数据损坏的替代方案要好。但还不如让一切都一成不变(上帝赞美 Haskell!)。

可惜!在 JavaScript 中,您总是在单线程上操作甚至网络工作者(每个都在单独的上下文中运行)。因此,由于您的执行上下文中不能有与线程相关的竞争条件(所有那些可爱的全局变量和闭包),支持不变性的要点就消失了。

(话虽如此,有一个优势,在网络工作者,这是,你有没有关于与主线程上的对象摆弄的预期使用纯函数。)

2)不变性可以(以某种方式)避免应用程序状态下的竞争条件。

这就是问题的真正症结所在,大多数(React)开发人员会告诉你,Immutability 和 FP 可以以某种方式发挥这种魔法,让你的应用程序的状态变得可预测。

当然,这并不意味着您可以避免数据库中的竞争条件,要解决这个问题,您必须协调所有浏览器中的所有用户,为此您需要一种后端推送技术,如WebSockets(更多关于此的内容),它将向运行该应用程序的每个人广播更改。

也不意味着 JavaScript 中存在一些固有问题,即您的应用程序状态需要不变性才能变得可预测,任何在 React 之前编写前端应用程序的开发人员都会告诉您这一点。

这个相当令人困惑的声明只是意味着,如果您使用 React,您的应用程序很容易出现竞争条件,但这种不变性可以让您消除这种痛苦为什么?因为 React 是特殊的......它首先被设计为一个高度优化的渲染库,状态管理颠覆了这个目标,因此组件状态是通过一个异步事件链(又名“单向数据绑定”)来管理的,这些事件优化了渲染,但您无法控制并依靠您记住不要直接改变状态......

鉴于这种情况,很容易看出对不变性的需求与 JavaScript 几乎没有关系,而与 React 有很大关系:如果在你的新应用程序中有一堆相互依赖的变化,并且没有简单的方法来弄清楚你的state 当前处于,你会感到困惑,因此使用不变性来跟踪每个历史变化是非常有意义的

3) 竞争条件绝对糟糕。

好吧,如果您使用 React,它们可能是。但是如果你选择不同的框架,它们就很少见了。

此外,你通常有更大的问题要处理......像依赖地狱这样的问题。就像一个臃肿的代码库。就像你的 CSS 没有被加载一样。就像一个缓慢的构建过程或被困在一个单一的后端,这使得迭代几乎不可能。就像没有经验的开发人员不了解正在发生的事情并把事情搞得一团糟一样。

你懂。现实。但是,嘿,谁在乎呢?

4) 不变性利用引用类型来减少跟踪每个状态变化对性能的影响。

因为说真的,如果你每次状态改变时都要复制东西,你最好确保你很聪明。

5) 不变性允许你撤销东西

因为呃……这是你的项目经理会要求的第一项功能,对吧?

6) 不可变状态与 WebSockets 结合有很多很酷的潜力

最后但并非最不重要的一点是,状态增量的积累与 WebSockets 相结合是一个非常引人注目的案例,它允许将状态作为不可变事件流轻松消耗......

一旦在这个概念上花一分钱(状态是一个事件流——而不是一组代表最新观点的粗略记录),不变的世界就变成了一个神奇的栖息地。一个超越时间本身事件来源的奇迹和可能性的土地并在完成后右这个绝对可以让实时应用程式EASI来完成,你只播事件给大家感兴趣,所以他们的流动建立自己表示本和自己的变化写回社区流动。

但在某些时候,您醒来并意识到所有的奇迹和魔法都不是免费的与你热切的同事不同,你的利益相关者(是的,付钱给你的人)很少关心哲学或时尚,而是很关心他们为打造可以销售的产品而支付的钱。最重要的是,它更难为不变性编码并且更容易破坏它,另外,如果您没有后端来支持它,那么拥有一个不变的前端就毫无意义。当(并且如果!)您最终说服您的利益相关者您应该通过像 WebSockets 这样推送技术发布和使用事件时,您会发现在生产中扩展是多么痛苦


现在给一些建议,你是否应该选择接受它。

使用 FP/Immutability 编写 JavaScript 的选择也是使您的应用程序代码库更大、更复杂和更难管理的选择。我强烈主张将这种方法限制在您的 Redux 减速器上,除非您知道自己在做什么……而且如果您无论如何都要继续使用不变性,那么将不变状态应用于整个应用程序堆栈,而不仅仅是应用程序堆栈客户端,否则您将错过它的真正value。

现在,如果你有幸能够在你的工作中做出选择,那么试着运用你的智慧(或不),做支付你报酬的人应该做的事情你可以根据你的经验、直觉或你周围发生的事情(诚然,如果每个人都在使用 React/Redux,那么有一个有效的论点是,找到一个资源来继续你的工作会更容易)。或者,您可以尝试恢复驱动开发炒作驱动开发方法。他们可能更适合你。

总之,可以说对于不变性的事情是,它令你的时尚与您同行,至少直到下一个热潮来临时,由此时你会很高兴地前进。


现在,在本次自我治疗之后,我想指出我已将此作为文章添加到我的博客 => JavaScript 中的不变性:逆势观点如果您有强烈的感觉,也想摆脱胸膛,请随时在那里回复;)。

我强烈怀疑关于不变性的观点与团队和代码库大小密切相关,我认为主要支持者是硅谷巨头并非巧合。话虽如此,我不同意:不变性是一门有用的学科,就像不使用 goto 是一门有用的学科一样。或者单元测试。或者TDD。或者静态类型分析。并不意味着你每次都这样做(尽管有些人这样做)。我还要说实用性与炒作是正交的:在有用/多余和性感/无聊的矩阵中,每个都有很多例子。"hyped" !== "bad"
2021-03-15 22:15:22
嗨@ftor,好点,在另一个方向上走得太远。然而,由于“javascript 支持不变性”的文章和论点如此丰富,我觉得我需要平衡一下。所以新手有一个相反的观点来帮助他们做出判断。
2021-03-15 22:15:22
我已经使用 React 和 Flux/Redux 两年多了,我完全同意你的观点,很好的回应!
2021-03-20 22:15:22
你好史蒂文,是的。当我考虑 immutable.js 和 redux 时,我有所有这些疑问。但是,你的答案是惊人的!它增加了很多value,并感谢您解决我怀疑的每一点。即使在不可变对象上工作了几个月之后,它现在也更加清晰/更好。
2021-03-30 22:15:22
内容丰富,标题精美。在我找到这个答案之前,我以为我是唯一持有类似观点的人。我认识到不变性的value,但让我烦恼的是它已经变成了一种压制所有其他技术的 教条(例如,有损于 2 路绑定,这对于在 KnockoutJS 中实现的输入格式非常有用)。
2021-04-03 22:15:22

问题是,为什么不变性如此重要?改变对象有什么问题?这不是让事情变得简单吗?

实际上,情况恰恰相反:可变性使事情变得更加复杂,至少从长远来看是这样。是的,它使您的初始编码更容易,因为您可以随心所欲地更改内容,但是当您的程序变大时,它就会成为一个问题——如果一个值改变了,是什么改变了它?

当你让一切都变得不可变时,这意味着数据不能再意外地改变了。您肯定知道,如果将值传递给函数,则无法在该函数中更改该值。

简而言之:如果您使用不可变值,则可以很容易地对您的代码进行推理:每个人都会获得您的数据的唯一* 副本,因此它不会受到干扰并破坏代码的其他部分。想象一下这让在多线程环境中工作变得多么容易!

注 1:根据您的操作,不变性会带来潜在的性能成本,但 Immutable.js 之类的东西会尽其所能进行优化。

注 2:万一您不确定,Immutable.js 和 ES6 的const含义非常不同。

通常情况下,我可以将对象添加到数组中。在这种情况下我如何实现?删除商店并重新创建它?将一个对象添加到数组中不是一个成本更低的操作吗?PS:如果这个例子不是解释不变性的正确方法,请让我知道什么是正确的实际例子。

是的,您的新闻示例非常好,您的推理完全正确:您不能只修改现有列表,因此需要创建一个新列表:

var originalItems = Immutable.List.of(1, 2, 3);
var newItems = originalItems.push(4, 5, 6);
我不反对这个答案,但它没有解决他的“我想从一个实际例子中学习”问题的一部分。有人可能会争辩说,对多个领域中使用的新闻标题列表的单一引用是一件好事。“我只需要更新一次列表,所有引用新闻列表的内容都会免费更新”——我认为一个更好的答案应该是像他提出的这样的常见问题,并展示一个使用不变性的有value的替代方案。
2021-03-14 22:15:22
Imagine how much easier this makes working in a multi-threaded environment! -> 其他语言还可以,但这在单线程 JavaScript 中不是优势。
2021-03-22 22:15:22
我很高兴答案有帮助!关于您的新问题:不要试图猜测系统 :) 在这种确切的情况下,称为“结构共享”的东西会显着减少 GC 抖动 – 如果列表中有 10,000 个项目并再添加 10 个,我相信是不可变的。 Node.js 将尽可能地重用以前的结构。让 Immutable.js 担心内存问题,你很可能会发现它变得更好。
2021-03-27 22:15:22
@JaredSmith 仍然是我的观点。FP 和 Immutability 是在多线程环境中避免数据损坏和/或资源锁定的非常有用的范例,但在 JavaScript 中则不然,因为它是单线程的。除非我遗漏了一些神圣的智慧,否则这里的主要权衡是你是否准备让你的代码更复杂(和更慢)以避免竞争条件......这比大多数人要少得多思考。
2021-03-27 22:15:22
@StevendeSalas 请注意,JavaScript 主要是异步和事件驱动的。它完全不受竞争条件的影响。
2021-04-08 22:15:22

尽管其他答案很好,但要解决您关于实际用例的问题(来自对其他答案的评论),让我们暂时跳出您正在运行的代码一分钟,看看您眼皮底下无处不在的答案:git如果每次推送提交时都覆盖存储库中的数据会发生什么

现在我们遇到了不可变集合面临的问题之一:内存膨胀。Git 足够聪明,不会在每次更改时简单地制作文件的新副本,它只是跟踪差异

虽然我不太了解 git 的内部工作原理,但我只能假设它使用与您引用的库类似的策略:结构共享。在幕后,库使用尝试或其他树来仅跟踪不同的节点。

这种策略对于内存数据结构也具有合理的性能,因为存在以对数时间运行的众所周知的树操作算法。

另一个用例:假设您想要在您的 web 应用程序上有一个撤消按钮。对于数据的不可变表示,实现这样的表示相对微不足道。但是如果你依赖变异,那就意味着你必须担心缓存世界的状态和进行原子更新。

简而言之,要为运行时性能和学习曲线的不变性付出代价。但是任何有经验的程序员都会告诉你调试时间比编写代码时间长一个数量级。您的用户不必忍受与状态相关的错误可能会抵消对运行时性能的轻微影响。

@Ski 它只是复杂,因为它不是默认设置。我通常不会在我的项目中使用 mori 或 immutable.js:我总是对接受第三方 deps 犹豫不决。但如果这是默认设置(la clojurescript)或者至少有一个选择加入的本机选项,我会一直使用它,因为当我在 clojure 中编程时,我不会立即将所有东西都塞进原子中。
2021-03-20 22:15:22
乔阿姆斯特朗会说不要担心性能,等几年,摩尔定律会为你解决这个问题。
2021-03-21 22:15:22
仅仅因为一个模式在 git 中有意义并不意味着同样的事情在任何地方都有意义。在 git 中,您实际上关心存储的所有历史记录,并且希望能够合并不同的分支。在前端,您不关心大部分状态历史,也不需要所有这些复杂性。
2021-03-24 22:15:22
我说一个绝妙的例子。我对不变性的理解现在更加清晰了。谢谢贾里德。实际上,其中一种实现是撤销按钮 :D 你让我的事情变得非常简单。
2021-03-29 22:15:22
@JaredSmith 你是对的,事情只会变得越来越小,资源也越来越有限。不过,我不确定这是否会成为 JavaScript 的限制因素。我们一直在寻找提高性能的新方法(例如 Svelte)。顺便说一下,我完全同意你的其他评论。使用不可变数据结构的复杂性或困难通常归结为语言没有对该概念的内置支持。Clojure 使不变性变得简单,因为它融入到语言中,整个语言都是围绕这个想法设计的。
2021-04-02 22:15:22

问题是,为什么不变性如此重要?改变对象有什么问题?这不是让事情变得简单吗?

关于可变性

从技术角度来看,可变性没有错。它很快,它正在重新使用内存。开发人员从一开始就习惯了它(我记得它)。可变性的使用存在问题,并且这种使用可能带来麻烦。

如果 object 不与任何东西共享,例如存在于函数的作用域中并且不暴露给外部,那么很难看到不变性的好处。真的在这种情况下,保持不变是没有意义的。当某些东西被共享时,不变性的感觉就开始了。

可变性头痛

可变的共享结构很容易造成许多陷阱。可以访问引用的任何代码部分的任何更改都会影响具有此引用可见性的其他部分。这种影响将所有部分连接在一起,即使它们不应该知道不同的module。一个函数中的突变可能会导致应用程序的完全不同部分崩溃。这样的事情是一个不好的副作用。

突变的下一个常见问题是状态损坏。当变异过程在中间失败时,可能会发生损坏状态,有些字段被修改,有些没有。

更重要的是,使用突变很难跟踪变化。简单的参考检查不会显示差异,要知道发生了什么变化,需要进行一些深入的检查。此外,为了监控变化,需要引入一些可观察的模式。

最后,变异是信任赤字的原因。如果可以变异,您如何确定某些结构具有所需的value。

const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }

如上例所示,传递可变结构总是可以通过具有不同的结构来完成。函数 doSomething 正在改变从外部给出的属性。对代码不信任,你真的不知道你拥有什么,你将拥有什么。所有这些问题的发生都是因为:可变结构表示指向内存的指针。

不变性是关于value的

不变性意味着变化不是在同一个对象、结构上完成的,而是用新的结构来表示的。这是因为引用不仅代表内存指针,还代表值。每一次变化都会创造新的value,并且不会触及旧的value。如此清晰的规则还给了信任和代码可预测性。函数可以安全使用,因为它们不是变异,而是用自己的值处理自己的版本。

使用值而不是内存容器可以确定每个对象都代表特定的不可更改的值,并且使用它是安全的。

不可变结构代表值。

我在中篇文章中更深入地探讨了这个主题 - https://medium.com/@macsikora/the-state-of-immutability-169d2cd11310