为啥 JavaScript 中的不变性如此重要(或需要)?

Posted

技术标签:

【中文标题】为啥 JavaScript 中的不变性如此重要(或需要)?【英文标题】:Why is immutability so important (or needed) in JavaScript?为什么 JavaScript 中的不变性如此重要(或需要)? 【发布时间】:2016-03-26 21:52:27 【问题描述】:

我目前正在研究React JS 和React Native 框架。在中途我遇到了 Immutability 或 Immutable-JS library,当时我正在阅读有关 Facebook 的 Flux 和 Redux 实施的信息。

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

举个例子,让我们考虑一个简单的新闻阅读器应用,其起始屏幕是新闻标题的列表视图。

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

【问题讨论】:

相关:programmers.stackexchange.com/questions/151733/… 不可变的数据结构和纯函数导致引用透明,从而更容易推理程序的行为。使用函数式数据结构时,您还可以免费获得回溯。 我提供了一个 Redux 观点 @bozzmob。 一般来说,将不稳定性作为函数范式的概念来学习可能会很有用,而不是试图认为 JS 与它有关。 React 是由函数式编程的爱好者编写的。你必须知道他们知道什么才能理解他们。 这不是必需的,但它确实提供了一些不错的权衡。 Mutable State is to Software as Moving Parts are to Hardware 【参考方案1】:

我最近一直在研究同一主题。我会尽力回答您的问题并尝试分享我到目前为止所学到的知识。

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

基本上,不变性提高了可预测性、性能(间接)并允许进行突变跟踪。

可预测性

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

性能

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

所有更新都返回新值,但内部结构共享给 大大减少内存使用(和 GC 抖动)。这意味着如果 您附加到具有 1000 个元素的向量,它实际上并没有创建 一个 1001 个元素长的新向量。很可能,内部只有少数 分配小对象。

您可以阅读有关此here 的更多信息。

突变跟踪

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

其他资源:

The Dao of Immutability Immutable Data Structures and javascript Immutability in JavaScript

如果我设置一个初始值的对象数组。我不能 操纵它。这就是不变性原理所说的,对吧?(正确 如果我错了我)。但是,如果我有一个新的 News 对象必须 被更新?在通常情况下,我可以将对象添加到 大批。在这种情况下我该如何实现?删除商店并重新创建它? 向数组中添加对象不是成本更低的操作吗?

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

我喜欢使用 Redux 作为示例,因为它包含不变性。它有一个单一的不可变状态树(称为store),其中所有状态更改都是通过调度由reducer处理的动作来显式地进行的,reducer接受前一个状态以及所述动作(一次一个)并返回下一个状态您的应用程序。你可以阅读更多关于它的核心原则here。

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

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

Arrays Objects

【讨论】:

@naomik 感谢您的反馈!我的目的是说明这个概念并明确表明对象没有被变异,而不一定要展示如何一直实现它。不过,我的例子可能有点混乱,我稍后会更新它。 @bozzmob 不客气!不,这是不正确的,您需要自己在减速器中强制执行不变性。这意味着您可以遵循视频中演示的策略或使用 immutablejs 之类的库。你可以找到更多信息here和here。 @naomik ES6 const 与不变性无关。 Mathias Bynens 写了一篇很棒的 blog article 关于它。 @terabaud 感谢分享链接。我同意这是一个重要的区别。 ^_^ 请解释这个“突变隐藏了变化,它会产生(意想不到的)副作用,这可能会导致讨厌的错误。当你强制执行不变性时,你可以保持你的应用程序架构和心理模型简单,这使得更容易关于你的申请的原因。”因为在 JavaScript 的上下文中这根本不是真的。【参考方案2】:

不变性的逆向观点

TL/DR:在 JavaScript 中,不变性更像是一种时尚趋势,而不是必需品。如果您使用 React,它确实为状态管理中的一些 confusing design choices 提供了一个巧妙的解决方法。然而在大多数其他情况下,它不会为它引入的复杂性增加足够的价值,为pad up a resume 提供更多服务而不是满足实际的客户需求。

长答案:阅读下文。

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

嗯,很高兴你问到!

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

时机恰到好处。 Angular 的新颖性正在消退,JavaScript 世界已准备好专注于具有适当酷度的最新事物,这个库不仅具有创新性,而且与 React 完美匹配,而 React 正在被另一个 @ 兜售987654328@.

虽然可悲的是,时尚在 JavaScript 世界中占据主导地位。现在阿布拉莫夫被誉为半神,而我们所有凡人都必须服从Dao of Immutability...不管这是否有意义。

改变对象有什么问题?

什么都没有!

事实上,程序员一直在变异对象,呃……只要有对象需要变异。 50+ years 换句话说就是应用程序开发。

为什么要把事情复杂化?当你有对象cat 并且它死了,你真的需要第二个cat 来跟踪变化吗?大多数人只会说cat.isDead = true 就可以了。

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

是的! .. 当然可以!

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

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

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

或者……

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

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

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

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

关于不变性,现在需要记住几个重要的点,你会以只有天真才能聚集的***强度将它们扔给你。

1) 不变性对于避免race conditions in multi-threaded environments非常有用。

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

但是唉!在 JavaScript 中你总是operate on a single thread。甚至网络工作者(每个都在separate context 中运行)。因此,由于在执行上下文(所有那些可爱的全局变量和闭包)中不能有 线程相关 竞争条件,支持不变性的主要观点就不再适用了。

(话虽如此,在 Web Worker 中使用纯函数一个优势,那就是你不会期望在主线程上摆弄对象。)

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

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

当然,这并不意味着您可以避免race conditions in the database,要实现这一目标,您必须协调所有浏览器中的所有用户,为此您需要一个后端推送技术,例如 WebSockets(更多内容见下文),它将向运行应用程序的每个人广播更改。

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

这个相当令人困惑的说法只是意味着如果您使用 React,您的应用程序很容易出现竞争条件,但这种不变性可以让您摆脱这种痛苦。为什么?因为 React 很特别.. 它首先被设计为带有状态管理 subverted to that aim 的 highly optimised rendering library,因此组件状态通过优化渲染的 asynchronous chain of events(又名“单向数据绑定”)进行管理,但是您无法控制和依赖你remembering not to mutate state directly...

在这种情况下,很容易看出对不变性的需求与 JavaScript 无关,而与 React 有很大关系:如果在你的新应用程序中有一堆相互依赖的变化,并且没有简单的方法来计算找出您当前的状态,you are going to get confused,因此使用不变性来跟踪每个历史变化是非常有意义的

3) 竞争条件非常糟糕。

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

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

你知道的。现实。但是,嘿,谁在乎呢?

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

因为说真的,如果您要在每次状态更改时都复制内容,那么您最好确保自己对此很聪明。

5) 不变性允许您撤消操作

因为呃..这是您的项目经理要求的第一个功能,对吧?

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

最后但并非最不重要的一点是,状态增量的累积使得与 WebSockets 的结合成为一个非常引人注目的案例,它允许轻松使用 state as a flow of immutable events...

一旦这个概念(状态是事件的流动——而不是代表最新观点的一组粗略记录)下降,不变的世界就变成了一个神奇的栖息地。一片充满event-sourced 奇迹和可能性的土地,超越了时间本身。如果做得好,这绝对可以让实时应用程序更容易完成er,您只需将事件流广播给所有感兴趣的人,这样他们就可以build their own representation 当前并将他们自己的更改写回社区流动。

但在某个时刻,你醒来并意识到所有的奇迹和魔法do not come for free。与你热心的同事不同,你的利益相关者(是的,付钱给你的人)很少关心哲学或时尚,而很关心他们为构建可以销售的产品而支付的钱。最重要的是,它更难为不变性编写代码并且更容易破坏它,而且如果你没有后端来支持它,那么拥有一个不变的前端就没有什么意义了。当(如果!)你最终说服你的利益相关者你应该通过 push techology 像 WebSockets 一样发布和消费事件,你会发现 pain it is to scale in production 是什么。


现在请教一些建议,如果您选择接受的话。

选择使用 FP/Immutability 编写 JavaScript 也是使您的应用程序代码库更大、更复杂且更难管理的选择。我强烈主张将这种方法限制在你的 Redux reducer 上,除非你知道你在做什么......如果你要继续使用不变性,那么应用immutable state to your whole application stack,而不仅仅是客户端,因为你错过了它的真正价值。

现在,如果您有幸能够在工作中做出选择,那么请尝试使用您的智慧(或不使用)和do what's right by the person who is paying you。你可以根据你的经验、直觉或你周围发生的事情来做这件事(诚然,如果每个人都在使用 React/Redux,那么有一个有效的论点是更容易找到资源来继续你的工作)。或者,您可以尝试Resume Driven Development 或Hype Driven Development 方法。它们可能更适合你。

简而言之,不变性要说的是,它让你在同龄人中变得时髦,至少在下一个热潮到来之前,到那时你会很高兴搬家开。


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

【讨论】:

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

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

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

当您将所有内容都设为不可变时,这意味着数据不能再被意外更改。您肯定知道,如果将值传递给函数,则无法在该函数中更改它。

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

注意 1:不可变性可能会降低性能,具体取决于您所做的事情,但 Immutable.js 之类的东西会尽其所能进行优化。

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

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

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

var originalItems = Immutable.List.of(1, 2, 3);
var newItems = originalItems.push(4, 5, 6);

【讨论】:

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

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

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

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

这种策略对于内存数据结构也相当有效,因为有well-known 树操作算法在对数时间内运行。

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

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

【讨论】:

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

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

关于可变性

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

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

易变性头痛

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

接下来经常出现的突变问题是损坏状态。当变异过程在中间失败时,可能会发生损坏状态,并且某些字段被修改,而某些字段没有被修改。

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

最后,突变是信任缺失的原因。如果可以突变,您如何确定某些结构具有想要的价值。

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

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

不变性是关于价值观

不变性意味着更改不是在同一个对象、结构上进行的,而是在新的对象、结构中表示更改。这是因为引用不仅代表内存指针的值。每一次改变都会创造新的价值,而不会触及旧的价值。如此清晰的规则可以恢复信任和代码的可预测性。函数使用起来很安全,因为它们不是变异,而是处理具有自己值的自己的版本。

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

不可变结构代表值。

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

【讨论】:

【参考方案6】:

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

可以在不同的上下文中跟踪不变性,但最重要的是根据应用程序状态和应用程序 UI 跟踪它。

我认为 JavaScript Redux 模式是非常流行和现代的方法,因为您提到了这一点。

对于 UI,我们需要使其可预测。 如果UI = f(application state) 是可以预见的。

应用程序(在 JavaScript 中)确实通过使用 reducer 函数实现的操作来改变状态。

reducer 函数简单地接受动作和旧状态并返回新状态,保持旧状态不变。

new state  = r(current state, action)

好处是:由于所有状态对象都已保存,因此您可以穿越状态,并且您可以在 UI = f(state) 之后的任何状态下渲染应用程序

因此您可以轻松撤消/重做。


碰巧创建所有这些状态仍然可以节省内存,与 Git 的类比非常好,我们在带有符号链接(基于 inode)的 Linux 操作系统中也有类似的类比。

【讨论】:

【参考方案7】:

Javascript 中不变性的另一个好处是它减少了时间耦合,这通常对设计有很大的好处。考虑具有两种方法的对象的接口:

class Foo 

      baz() 
          // .... 
      

      bar() 
          // ....
      



const f = new Foo();

可能需要调用baz() 才能使对象处于有效状态,以便调用bar() 才能正常工作。但是你怎么知道的呢?

f.baz();
f.bar(); // this is ok

f.bar();
f.baz(); // this blows up

要弄清楚,您需要仔细检查类的内部结构,因为通过检查公共接口并不能立即看出这一点。这个问题可能会在包含大量可变状态和类的大型代码库中爆发。

如果Foo 是不可变的,那么这不再是问题。假设我们可以按任意顺序调用bazbar 是安全的,因为类的内部状态不能改变。

【讨论】:

【参考方案8】:

曾几何时,线程之间的数据同步存在问题。这个问题很痛苦,有10多个解决方案。有些人试图从根本上解决它。那是函数式编程诞生的地方。就像马克思主义一样。我不明白 Dan Abramov 是如何将这个想法卖给 JS,因为它是单线程的。他是个天才。

我可以举一个小例子。 gcc中有一个属性__attribute__((pure))。如果您不特别清除它,编译器会尝试解决您的函数是否为纯函数。即使您的状态是可变的,您的功能也可以是纯的。不变性只是 100 多种方法中的一种,可以保证您的功能是纯粹的。实际上 95% 的函数都是纯函数。

如果您实际上没有严重的理由,则不应使用任何限制(例如不变性)。如果您想“撤消”某些状态,您可以创建交易。如果您想简化通信,您可以使用不可变数据发送事件。这取决于你。

我正在写这篇来自后马克思主义***的信息。我确信任何想法的激进化都是错误的方式。

【讨论】:

第三段非常有意义。谢谢你。 '如果你想“撤消”某些状态,你可以创建交易'!! 顺便说一句,OOP 也可以与马克思主义进行比较。还记得Java吗?见鬼,JavaScript 中的 Java 的奇怪之处?炒作从来都不是好事,它会导致激进化和两极分化。从历史上看,OOP 比 Facebook 对 Redux 的大肆宣传要大得多。虽然他们确实尽力了。【参考方案9】:

不同的看法...

我的另一个答案从非常实际的角度解决了这个问题,我仍然喜欢它。我决定将其添加为另一个答案,而不是该答案的附录,因为这是一个无聊的哲学咆哮,希望也能回答这个问题,但并不真正符合我现有的答案。

TL;DR

即使在小型项目中,不变性也很有用,但不要以为它存在就意味着它适合你。

更多,更长的答案

注意:为了回答这个问题,我使用“纪律”这个词来表示为了某些利益而自我否定。

这在形式上类似于另一个问题:“我应该使用 Typescript 吗?为什么类型在 JavaScript 中如此重要?”。它也有类似的答案。考虑以下场景:

您是大约 5000 行 JavaScript/CSS/html 代码库的唯一作者和维护者。你的半技术老板读了一些关于 Typescript-as-the-new-hotness 的内容,并建议我们可能想要转向它,但将决定权留给你。所以你阅读它,玩它等等。

所以现在你有一个选择,你要改用 Typescript 吗?

Typescript 具有一些引人注目的优势:智能感知、及早发现错误、预先指定您的 API、在重构中断时易于修复、更少的测试。 Typescript 也有一些成本:某些非常自然和正确的 JavaScript 习惯用法在它不是特别强大的类型系统中建模可能很棘手,注释会增加 LoC,重写现有代码库的时间和精力,构建管道中的额外步骤等。更根本的是,它从可能的正确 JavaScript 程序中划分出一个子集,以换取您的代码更有可能正确的承诺。这是任意限制的。这就是重点:你强加了一些限制你的纪律(希望不要打自己的脚)。

回到这个问题,在上一段的上下文中重新表述:它值得吗?

在所描述的场景中,我认为如果您非常熟悉中小型 JS 代码库,那么选择使用 Typescript 更美观而不是实用。这很好,美学没有什么错误,只是它们不一定引人注目。

场景 B:

您换了工作,现在是 Foo Corp 的一名业务线程序员。您正在与一个 10 人的团队合作,开发一个 90000 个 LoC(并且还在增加)的 JavaScript/HTML/CSS 代码库,其中包含一个相当复杂的构建管道,涉及babel、webpack、一套 polyfills、对各种插件做出反应、一个状态管理系统、~20 个第三方库、~10 个内部库、编辑器插件(如带有内部样式指南规则的 linter)等。

当你还是 5k LoC 的男孩/女孩时,这并不重要。即使是文档 也没什么大不了的,即使在 6 个月后回到代码的特定部分,你也可以很容易地弄清楚它。但是现在纪律不仅很好,而且必要。该学科可能不涉及 Typescript,但可能涉及某种形式的静态分析以及所有其他形式的编码学科(文档、样式指南、构建脚本、回归测试、CI)。纪律不再是奢侈品,而是必需

所有这一切都适用于 1978 年的 GOTO:你在 C 语言中的极小二十一点游戏可以使用 GOTOs 和意大利面条逻辑,选择你自己的冒险方式并不是什么大不了的事通过它,但随着程序变得更大和更雄心勃勃,嗯,GOTO无纪律使用无法持续。所有这些都适用于今天的不变性。

就像静态类型一样,如果您不是在由工程师团队维护/扩展它的大型代码库上工作,那么使用不变性的选择比实际更美观:它的好处仍然存在,但可能不会超过成本.

但与所有有用的学科一样,有一点它不再是可选的。如果我想保持健康的体重,那么涉及冰淇淋的纪律可能是可选的。但如果我想成为一名有竞争力的运动员,我对是否吃冰淇淋的选择就包含在我对目标的选择中。如果你想用软件改变世界,不变性可能是你需要避免它在自身重量下崩溃的一部分。

【讨论】:

+1 我喜欢。更多关于 Jared 的观点。然而,不变性并不能使团队免于缺乏纪律。 ? @StevendeSalas 这是一种纪律。因此,我认为它与其他形式的软件工程学科相关(但不会取代)。它是补充,而不是取代。但是正如我在对您的回答的评论中所说的那样,我一点也不感到惊讶,它是由一个科技巨头推动的,一群工程师都在同一个庞大的代码库上努力工作:) 他们需要他们能得到的所有纪律。在大多数情况下,我不会改变对象,但也不会使用任何形式的强制执行,因为这只是我自己。【参考方案10】:

举个例子:

const userMessage  =  
   user: "userId",
   topic: "topicId"
   content: 


validateMessage(userMessage)
saveMessage(userMessage) 
sendMessageViaEmail(userMessage)
**sendMessageViaMobilePush(userMessage)** 

console.log(userMessage) // => ?

现在回答一些问题:

    mutable 代码中的 userMessagesendMessageViaMobilePush(userMessage)) 下是什么?


    id: "xxx-xxx-xxx-xxx", //set by ..(Answer for question 3)
    user:"John Tribe",     //set by sendMessageViaEmail
    topic: "Email title",  //set by sendMessageViaEmail
    status: FINAL,         //set by saveMessage or could be set by sendMessageViaEmail
    from: "..",            //set by sendMessageViaEmail
    to:"...",              //set by sendMessageViaEmail
    valid:true,            //set by validateMessage
    state: SENT             //set by sendMessageViaEmail


Surprised?? Me too :d. But this is normal with mutability in javascript. 
(in Java too but a bit in different way. When You expect null but get some object).  

    immutable 代码中同一行的 userMessage 下是什么?

    const userMessage  =    
        user: "userId",
        topic: "topicId",
        content: 
    
    
    Easy right ?

    你能猜出 Snippet 1mutable 代码中的“id”是通过哪种方法更新的吗?

    By sendMessageViaEmail. 
    
    Why? 
    
    Why not?
    
    Well it was at first updated by saveMessage, 
    but then overridden by sendMessageViaEmail.

    可变代码中,人们没有收到推送消息(sendMessageViaMobilePush)。你能猜到为什么吗??

    because I am amazing developer :D and I put safety check in method sendMessageViaMobilePush(userMessage) 
    
    function sendMessageViaMobilePush(userMessage) 
        if (userMessage.state != SENT)   //was set to SENT by sendMessageViaEmail
             send(userMessage)
        
    
    
    Even if You saw this method before, 
    was this possible for You to predict this behavior in mutable code ? 
    For me it wasn't. 

希望这有助于您了解在 javascript 中使用可变对象的主要问题。

请注意,当复杂性增加时,很难检查设置的内容和位置,尤其是当您与其他人一起工作时。

【讨论】:

【参考方案11】:

我为可变(或不可变)状态创建了一个与框架无关的开源 (MIT) 库,它可以替换所有那些不可变存储,如库(redux、vuex 等...)。

不可变状态对我来说很难看,因为要做的工作太多(对于简单的读/写操作有很多操作),代码的可读性较差并且大数据集的性能不可接受(整个组件重新渲染:/ )。

使用deep-state-observer,我只能使用点符号更新一个节点并使用通配符。我还可以创建状态历史记录(撤消/重做/时间旅行),只保留那些已更改的具体值 path:value = 更少的内存使用。

有了deep-state-observer,我可以对事物进行微调,并且我可以对组件行为进行粒度控制,因此可以显着提高性能。代码更具可读性,重构也更容易——只需搜索和替换路径字符串(无需更改代码/逻辑)。

【讨论】:

【参考方案12】:

我认为支持不可变对象的主要原因是保持对象的状态有效。

假设我们有一个名为arr 的对象。当所有项目都是相同的字母时,此对象有效。

// this function will change the letter in all the array
function fillWithZ(arr) 
    for (var i = 0; i < arr.length; ++i) 
        if (i === 4) // rare condition
            return arr; // some error here

        arr[i] = "Z";
    

    return arr;


console.log(fillWithZ(["A","A","A"])) // ok, valid state
console.log(fillWithZ(["A","A","A","A","A","A"])) // bad, invalid state

如果arr 成为不可变对象,那么我们将确保 arr 始终处于有效状态。

【讨论】:

我认为每次调用 fillWithZarr 都会发生变异 如果你使用immutable.js,每次你改变它都会得到一个新的对象副本。所以原始对象保持不变

以上是关于为啥 JavaScript 中的不变性如此重要(或需要)?的主要内容,如果未能解决你的问题,请参考以下文章

Scala 的不变性是不是意味着无法修改列表或无法修改列表及其内容?

字符串的不变性

CNN网络中的不变性理解

React中的不可变性

关于最佳实践的问题:React Native 的不变性和重新渲染

VMware迁移的真实教训:为啥备份如此重要