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

273

我目前正在使用 React JSReact Native 框架进行开发。在半路上,我遇到了不可变性或者Immutable-JS库,当我在阅读Facebook的Flux和Redux实现时。

问题是,为什么不可变性那么重要?改变对象有什么不对吗?这难道不会使事情更加简单吗?

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

如果我设置一个对象数组初始值,我就不能对它进行操作。这就是不可变性原则所说的,对吗?(请纠正我如果我错了)。 但是,如果我有一个需要更新的新闻对象呢?通常情况下,我可以把这个对象添加到数组中。 在这种情况下,我该怎么办?删除存储并重新创建它吗? 把一个对象添加到数组中不是一个更少消耗资源的操作吗?


9
相关链接:http://programmers.stackexchange.com/questions/151733/if-immutable-objects-are-good-why-do-people-keep-creating-mutable-objects为什么人们会不断创建可变对象?如果不可变对象是好的,那么这些对象为什么还存在? - Mulan
3
不变数据结构和纯函数会导致引用透明性,使得程序行为的推理变得更加简单。在使用函数式数据结构时,您还可以免费获得回溯能力。 - WorBlux
我在@bozzmob提供了一个Redux的观点。 - prosti
1
学习不变性在函数式编程中作为一个概念可能很有用,而不是尝试认为JS与之相关。React是由函数式编程的粉丝编写的。要理解他们,你必须知道他们知道的内容。 - Gherman
这不是必要的,但它确实提供了一些不错的折衷方案。可变状态对于软件就像运动部件对于硬件一样 - Kristian Dupont
从ReactJS的角度来思考。如果您返回具有不同内容的相同对象,它如何知道您添加了一个项目。您必须告诉它数组已更改并且需要重新渲染。如果在某些深度嵌套数据中发生更改(例如建议计数),则比较会很慢,因为您需要遍历整个数组。但是使用不可变原则,您别无选择,只能返回新对象。现在,React框架可以通过执行指针地址比较轻松地知道它需要重新渲染,这非常快速。 - yiwen
13个回答

240
我最近一直在研究同样的话题。我会尽力回答你的问题,并尝试分享我目前所学到的知识。
问题是,为什么不变性很重要?改变对象有什么问题吗?这难道不会让事情变得简单吗?
基本上,这归结于不变性增加了可预测性、性能(间接)并允许进行变异追踪。
可预测性
变异隐藏了变化,从而产生(意想不到的)副作用,可能会导致严重的错误。当你强制执行不变性时,你可以保持应用程序架构和心理模型的简单性,这使得你更容易推理出你的应用程序。
性能
即使将值添加到不可变对象中意味着需要创建一个新实例,其中需要复制现有值并将新值添加到新对象中,这会耗费内存,但不可变对象可以利用结构共享来减少内存开销。
所有更新都返回新值,但内部结构是共享的,以极大地减少内存使用(和GC抖动)。这意味着,如果您向具有1000个元素的向量附加,则实际上不会创建长度为1001的新向量。很可能,在内部只分配了一些小对象。
您可以在此处阅读更多信息。 变异跟踪 除了减少内存使用外,不可变性还允许您通过使用引用和值相等来优化应用程序。这使得非常容易看到是否发生了任何更改。例如,React组件中的状态更改。您可以使用shouldComponentUpdate通过比较状态对象来检查状态是否相同,并防止不必要的渲染。
您可以在此处阅读更多信息。
其他资源:

如果我最初设置一个对象值的数组,我就不能操纵它。这就是不变性原则所说的,对吗?(如果我错了,请纠正我)。但是,如果我有一个新的News对象需要更新怎么办?通常情况下,我只需将对象添加到数组中即可。在这种情况下,我该如何实现?删除存储并重新创建它吗?将对象添加到数组中不是一项更少的操作吗?

是的,这是正确的。如果您不清楚如何在应用程序中实现此功能,我建议您查看redux如何实现此功能,以熟悉核心概念,这对我很有帮助。

我喜欢使用Redux作为示例,因为它支持不可变性。它有一个单一的不可变状态树(称为store),所有状态更改都通过分派操作显式进行,这些操作由接受先前状态和操作的减速器处理(一次一个),并返回应用程序的下一个状态。您可以在此处阅读更多关于其核心原则。

Egghead.io上有一门出色的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();

此外,这些视频进一步演示了如何实现以下内容的不可变性:

1
@naomik 感谢您的反馈!我的意图是阐明这个概念并明确表明对象没有被改变,而不一定是展示如何完全实现它。然而,我的例子可能有点令人困惑,我会稍后更新它。 - danillouz
2
@bozzmob 不用谢!不,那不正确,你需要在reducer中自己实现不可变性。这意味着你可以采用视频中演示的策略或使用像immutablejs这样的库。你可以在这里找到更多信息:(https://dev59.com/0VwZ5IYBdhLWcg3wFMoR)和(http://www.ackmanndickenson.com/2015/11/blog-redux-and-immutable-js-take-center-stage/)。 - danillouz
1
@naomik ES6中的const并不是关于不可变性的。Mathias Bynens写了一篇很棒的博客文章来解释这个问题。 - Lea Rosema
1
@terabaud 感谢分享链接。我同意这是一个重要的区别。^_^ - Mulan
7
“Mutation hides change, which create (unexpected) side effects, which can cause nasty bugs. When you enforce immutability you can keep your application architecture and mental model simple, which makes it easier to reason about your application.” 这句话的意思是“改变被隐藏了,可能会带来意外的副作用,导致严重的错误。如果你强制使用不可变性,可以保持应用程序架构和心智模型简单,从而更容易理解应用程序。”然而在 JavaScript 的上下文中,这句话并不完全正确。 - Pavle Lekic
显示剩余3条评论

202

对不可变性的反向观点

简而言之:在JavaScript中,不可变性更多地是一种时尚潮流,而非必需品。如果你正在使用React,它确实为一些令人困惑的设计选择提供了一个巧妙的解决方案来管理状态。然而,在大多数其他情况下,它并不能带来足够的价值,而只会引入复杂性,更多地用于填充简历,而不是满足实际客户需求。

长篇回答请见下文。

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

好吧,我很高兴你问到了这个问题!

不久前,一个非常有才华的家伙叫做Dan Abramov写了一个名为Redux的JavaScript状态管理库,它使用纯函数和不可变性。他还制作了一些非常酷的视频,使这个想法变得非常容易理解(并且销售)。

时机恰到好处。Angular的新奇感正在消退,而JavaScript世界正准备专注于最新的、具有适度酷炫程度的东西,而这个库不仅创新,而且与另一个硅谷巨头推销的React完美契合。

尽管令人沮丧,但在JavaScript世界中,时尚统治着。现在,Abramov被称为半神,并且我们这些凡人必须屈服于不可变性之道……无论它是否有意义。

改变对象有何不妥之处?

没有!

实际上,程序员一直以来都在改变对象,嗯... 自从存在对象以来已经有了50多年的应用开发。

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

(改变对象)难道不让事情变得简单吗?

是的!..当然会!

在JavaScript中,特别是在实践中,最常用于呈现某些在其他地方维护的状态的视图(比如数据库)。
如果我有一个需要更新的新闻对象...在这种情况下我该怎么做呢?删除存储并重新创建它吗?将对象添加到数组中不是一种更廉价的操作吗?
嗯,你可以采用传统的方法,更新新闻对象,这样你内存中表示的对象就会改变(用户看到的视图也会相应改变,或者希望如此)...
或者...
你可以尝试时髦的函数式编程/不可变性方法,将你的更改添加到新闻对象的一个跟踪每个历史更改的数组中,然后通过遍历数组来确定正确的状态表示应该是什么(哇!)。
我正在努力学习什么是正确的。请给我指点一下吧 :)
时尚来了又走,伙计。有很多种方法可以解决问题。
很抱歉你必须忍受不断变化的编程范式的困惑。但是,欢迎加入这个俱乐部!
现在有几个关于不可变性的重要要点需要记住,而且你会以只有天真才能拥有的狂热强度被抛出来。
1)不可变性对于避免多线程环境中的竞态条件非常棒。
多线程环境(如C++、Java和C#)在多个线程想要更改对象时会锁定它们。这对性能来说是不好的,但比数据损坏的替代方案要好。然而,并不像使所有东西都不可变那样好(上帝保佑Haskell!)。
但是哎呀!在JavaScript中,你总是在单个线程上操作。即使是Web Worker(每个运行在一个单独的上下文中)。所以,由于你不能在执行上下文中出现与线程相关的竞态条件(所有那些可爱的全局变量和闭包),不可变性的主要优点就不存在了。

(话虽如此,在 Web Workers 中使用纯函数也有优势,就是你不需要对主线程上的对象进行调整的期望。)

2) 不可变性(Immutability)可以(在某种程度上)避免应用程序状态的竞态条件。

这才是关键,大多数(React)开发者会告诉你,不可变性和函数式编程可以以某种方式实现让应用程序状态变得可预测的魔法。

当然,这并不意味着你可以避免数据库中的 竞态条件,要做到这一点,你需要协调所有浏览器中的所有用户,为此,你需要像WebSockets这样的后端推送技术(下面将详细介绍),它将向运行该应用程序的每个人广播更改。

这也不意味着 JavaScript 存在某种固有问题,需要通过不可变性来使应用程序状态变得可预测,任何在 React 之前编写过前端应用程序的开发者都会告诉你这一点。

这个相当令人困惑的说法简单地意味着,如果你使用React,你的应用程序容易出现竞态条件,但是不可变性可以帮助你摆脱这种痛苦。为什么?因为React很特殊...它首先被设计为一个高度优化的渲染库,而状态管理则被削弱到这个目标,因此组件状态通过异步事件链(也称为"单向数据绑定")进行管理,以优化渲染,但你无法控制它,并且依赖于你记住不直接改变状态...
在这种情况下,很容易看出不可变性与JavaScript关系不大,而与React有很大关系:如果您的全新应用程序中存在一堆相互依赖的更改,并且没有简单的方法来确定您当前的状态,您将感到困惑,因此使用不可变性来跟踪每个历史更改是完全合理的

3) 竞态条件绝对是不好的。

嗯,如果您使用React可能会出现这种情况。但是,如果选择其他框架,它们就很少见。

此外,您通常还有更大的问题要处理...像依赖地狱这样的问题。像臃肿的代码库。像CSS加载不成功。像缓慢的构建过程或者被困在导致迭代几乎不可能的单块式后端。像经验不足的开发人员不明白发生了什么,并把事情搞得一团糟。

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

4) 不可变性利用引用类型来减少跟踪每个状态变化的性能影响。
因为说真的,如果你每次状态改变都要复制东西,那你最好确保自己聪明一些。
5) 不可变性允许你撤销操作。
因为嗯...这是你的项目经理最想要的功能,对吧?
6) 不可变状态与WebSockets结合具有很多潜力。
最后但并非最不重要的是,状态增量的累积与WebSockets相结合,可以轻松消费作为不可变事件流的状态,这是一个非常有说服力的案例...

一旦这个概念(状态是事件的流动,而不是粗糙的记录集合代表最新视图)被理解,不可变的世界就变成了一个令人惊叹的居住之地。一个超越时间本身的奇迹和可能性之地,一个event-sourced的乐土。当正确实施时,这可以使实时应用程序更容易实现,只需将事件的流动广播给所有感兴趣的人,他们可以建立自己的表示并将自己的变化写回到共同的流中。

但在某个时候,你会醒悟到所有那些奇妙和魔力并非免费获取。与你热情的同事不同,你的利益相关者(是的,就是支付你工资的人)对哲学或时尚不太关心,他们更在意为产品付费建设所花费的金钱。底线是,编写不可变代码更加困难,而破坏它则更容易,此外,如果没有后端支持,拥有不可变前端也毫无意义。当(如果!)你最终说服你的利益相关者通过推送技术如WebSockets发布和消费事件时,你会发现在生产环境中扩展的困难之处


现在给你一些建议,如果你选择接受的话。
选择使用FP/Immutability来编写JavaScript代码也意味着让你的应用程序代码库变得更大、更复杂和更难管理。我强烈建议将这种方法限制在Redux reducers中,除非你知道自己在做什么... 如果你无论如何都要使用不可变性,那么请将不可变状态应用到整个应用程序堆栈,而不仅仅是客户端。毕竟,如果前端是不可变的,然后将其连接到一个数据库,其中所有记录都有一个可变版本,那就回到了你试图摆脱的同样问题!

现在,如果你有幸能在工作中作出选择,那么请尽量运用你的智慧(或者不运用),为支付你工资的人做正确的事情。你可以基于自己的经验、直觉,或者周围发生的事情来决策(不可否认的是,如果每个人都在使用React/Redux,那么有一个合理的论点是更容易找到资源继续你的工作)。另外,你也可以尝试以简历为驱动的开发以炒作为驱动的开发方法。它们可能更适合你。

简而言之,关于不变性的好处就是,至少会让你在同行中时尚一段时间,直到下一个狂热出现,到那时你会庆幸继续前进。


现在,在这次自我疗法之后,我想指出我已将其添加为我的博客文章 => JavaScript中的不变性:一个反对者的观点。如果你也有强烈的感受想要发表,请随意在那里回复 ;)。

14
您好Steven,是的。当我考虑immutable.js和redux时,我也有这些疑虑。但是,您的回答非常棒!它增加了很多价值,并且感谢您解决了我所有怀疑的问题。即使我已经使用不可变对象工作了数月,现在它更加清晰/更好了。 - bozzmob
6
我已经使用React和Flux/Redux超过两年了,我非常认同你的观点,回答得很好! - Pavle Lekic
9
我强烈怀疑对于不可变性的看法与团队和代码库的大小有很大关联,而主要支持者是硅谷巨头也不是巧合。尽管如此,我尊重地持不同意见:不可变性像不使用goto、单元测试、TDD或静态类型分析一样是一种有用的纪律。这并不意味着你必须始终如一地遵守它们(虽然有些人确实是这样做的)。我还想说的是,实用性与炒作无关:在有用/多余和时髦/无聊的矩阵中,有许多例子。 “‘炒作’并非‘糟糕’”。 - Jared Smith
6
嗨@ftor,说得好,有时候会走向另一个极端。然而,由于存在大量关于“JavaScript中不可变性”的文章和论点,我觉得需要平衡一下。这样新手就可以有一个相反的观点来帮助他们做出判断。 - Steven de Salas
6
信息量大,标题巧妙。在找到这个答案之前,我以为只有我持有类似的观点。我认识到不可变性的价值,但令我困扰的是它已经成为了一个压制其他技术的教条(例如对于双向绑定的负面影响,而双向绑定在如KnockoutJS中实现的输入格式化方面非常有用)。 - webketje
显示剩余12条评论

59
问题是,为什么不变性很重要?改变对象有什么问题吗?它不会让事情变得简单吗?
实际上,相反是真的:至少从长远来看,可变性会使事情变得更加复杂。是的,它可以使你最初的编码变得更容易,因为你可以随时更改任何你想要的东西,但当你的程序变得更大时,它就成为了一个问题——如果一个值发生了改变,是什么改变了它?
当你把所有东西都变成不可变的时候,这意味着数据不再能够被意外地改变。你可以确信,如果你把一个值传递给一个函数,在那个函数中它就无法被改变。
简单地说:如果你使用不可变的值,那么很容易理解你的代码:每个人都会得到你的数据的一个独特副本,所以它就不可能对它进行调整并破坏你代码的其他部分。想象一下,在多线程环境中工作会变得更加容易!
注1:根据你正在做的事情,不可变性可能会带来潜在的性能成本,但 Immutable.js 等东西已经尽力进行了优化。
注2:如果你不确定,Immutable.js 和 ES6 的 const 意义非常不同。
通常情况下,我可以只是将对象添加到数组中。在这种情况下,我该怎么做?删除存储并重新创建它吗?将对象添加到数组中是否是一种更便宜的操作?附注:如果这个例子不是解释不变性的正确方式,请告诉我什么是正确的实际例子。
是的,你的新闻例子是完全正确的,而且你的推理也是完全正确的:你不能仅仅修改现有列表,所以你需要创建一个新的列表:
var originalItems = Immutable.List.of(1, 2, 3);
var newItems = originalItems.push(4, 5, 6);

1
我不反对这个答案,但它没有解决他问题中的“我想从实际例子中学习”的部分。有人可能会认为,在多个区域使用新闻标题列表的单个引用是一件好事。“我只需要更新列表一次,所有引用新闻列表的内容都可以免费更新”- 我认为更好的答案应该提供一个像他所提出的常见问题,并展示一个使用不可变性的有价值的替代方案。 - Mulan
1
很高兴答案有帮助!关于你的新问题:不要试图去猜测系统 :) 在这种情况下,一种叫做“结构共享”的东西可以显著减少GC抖动——如果你有一个包含10,000个项目的列表,并添加了10个项目,我相信Immutable.js会尽可能地重用先前的结构。让Immutable.js来处理内存问题,你会发现它表现得更好。 - TwoStraws
7
在单线程的 JavaScript 中,这并不是一个优势,但想象一下在多线程环境中工作会更加容易! - Steven de Salas
1
请注意,JavaScript 主要是异步和事件驱动的。它并不完全免疫竞态条件。 - Jared Smith
1
@JaredSmith,我的观点仍然存在。FP(函数式编程)和Immutability(不可变性)是非常有用的范例,可以避免在多线程环境下出现数据损坏和/或资源锁定,但在JavaScript中并非如此,因为它是单线程的。除非我错过了某些至高无上的智慧,否则这里的主要权衡是您是否准备使您的代码更加复杂(和更慢),以避免竞态条件......这远比大多数人想象的要少成为问题。 - Steven de Salas
显示剩余4条评论

39
尽管其他答案都很好,但为了回答您在其他答案评论中提出的一个实际用例的问题,请走出您正在运行的代码一分钟,并查看您眼前无处不在的答案:git。如果每次推送提交时您覆盖存储库中的数据会发生什么?
现在我们遇到了不可变集合面临的问题之一:内存膨胀。Git 聪明地不会在每次更改时简单地创建文件的新副本,它只是跟踪差异
虽然我不太了解 Git 的内部工作原理,但我只能假设它使用与您引用的库类似的策略:结构共享。在底层,这些库使用tries或其他树来仅跟踪不同的节点。
这种策略对于内存中的数据结构也是相当高效的,因为存在众所周知的树操作算法,它们可以在对数时间内操作。
另一个用例:假设您想在 Web 应用程序上添加撤消按钮。对于您数据的不可变表示,实现这样的操作相对容易。但是如果您依赖于突变,则意味着您必须担心缓存世界状态并进行原子更新。
简而言之,在运行时性能和学习曲线方面,不可变性需要付出代价。但任何有经验的程序员都会告诉您,调试时间比编写代码的时间重要得多。而与用户不必忍受的状态相关的错误相比,运行时性能的轻微下降可能更为重要。

1
一个很棒的例子。我的不可变性理解更加清晰了。谢谢Jared。实际上,其中一个实现是撤销按钮:D而你让事情变得非常简单。 - bozzmob
5
只因为在git中某个模式有意义,并不意味着同样的事情在其他地方也有意义。在git中,您实际上关心存储的所有历史记录,并且希望能够合并不同的分支。但在前端,您并不关心大多数状态历史记录,也不需要所有这些复杂性。 - Ski
3
@Ski 这只是因为它不是默认设置,所以才显得复杂。我通常不在我的项目中使用mori或immutable.js:我总是犹豫是否要采用第三方依赖。但如果那是默认设置(如clojurescript),或者至少有一个选择原生选项,我会一直使用它,因为当我比如在clojure中编程时,我不会立即将所有东西都塞进atoms中。 - Jared Smith
Joe Armstrong会说不要担心性能,只需等几年,摩尔定律就会为你解决这个问题。 - ximo
@ximo摩尔定律遇到了一些问题,其中最主要的是世界已经转向移动设备,这些设备对散热的限制比晶体管更大。话虽如此,JS大多数情况下都受到IO限制。 - Jared Smith
1
@JaredSmith 你说得对,事情只会变得越来越小和资源受限。但我不确定这是否会成为JavaScript的限制因素。我们不断寻找提高性能的新方法(例如Svelte)。顺便说一下,我完全同意你的另一个评论。使用不可变数据结构的复杂性或难度通常归结于语言没有内置支持该概念。Clojure使不可变性变得“简单”,因为它是内置在语言中的,整个语言都是围绕这个想法设计的。 - ximo

10
问题是,为什么不可变性如此重要?改变对象有什么问题吗?这难道不会使事情变得简单吗?
关于可变性: 从技术角度来看,可变性没有问题。它很快,它重新使用内存。开发人员从一开始就习惯了它(据我所记)。问题存在于可变性的使用以及此使用可能带来的麻烦中。
如果对象没有与任何其他东西共享,例如存在于函数的范围内并且未暴露给外部,则很难看到不可变性的好处。在这种情况下,真的没有必要是不可变的。当涉及到共享时,不可变性的意义开始出现。
可变共享结构很容易产生许多陷阱。对引用具有访问权限的代码的任何更改都会影响到具有该引用的其他部分的可见性。这种影响将所有部分连接在一起,即使它们不应该知道不同模块的存在也是如此。一个函数中的变异可以完全崩溃应用程序中的另一个部分。这种事情是副作用。
下一个经常出现的变异问题是状态损坏。当变异过程在中途失败时,可能会出现损坏的状态,并且某些字段已修改,某些字段未修改。
此外,使用变异很难跟踪变化。简单的引用检查不会显示差异,要知道发生了什么变化,需要进行一些深入的检查。还需要引入一些可观测的模式来监视变化。
最后,变异是信任赤字的原因。如果结构可以被变异,您如何确定其具有所需的值。
const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }

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

不变性关乎值

不可变性意味着更改不是在同一个对象、结构上进行的,而是在新的对象、结构上进行的。这是因为引用不仅表示内存指针,还表示值。每次更改都会创建一个新值,而不会触及旧值。这些明确的规则让代码具有可预测性和信任度。函数是安全使用的,因为它们处理自己版本的自己值,而不是突变。

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

不可变结构体代表值。

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


8

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

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

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

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

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

reducer函数只需获取操作和旧状态并返回新状态,保持旧状态不变即可。

new state  = r(current state, action)

enter image description here

受益之处在于:由于所有状态对象均已保存,因此您可以时光旅行各个状态,并且可以在任何状态下渲染应用程序,因为 UI = f(state)。因此,您可以轻松地撤销/重做。
创建所有这些状态时,仍然可以在内存使用方面高效,与Git的类比非常好,在Linux操作系统中,我们也有类似的符号链接类比(基于inode)。

5
在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,因为类的内部状态不会改变。

4
从前,线程之间的数据同步问题一直是个大问题,有10多种解决方案。有些人试图从根本上解决这个问题。函数式编程就是在这样的背景下诞生的。这就像马克思主义一样。我不明白丹·阿布拉莫夫是如何将这个想法引入JS的,因为JS是单线程的。他真是个天才。
我可以给一个小例子。在gcc中有一个属性__attribute__((pure))。如果你没有特别声明,编译器会尝试解决你的函数是否纯洁的问题。即使你的状态是可变的,你的函数也可以是纯洁的。不可变性只是保证函数纯洁性的100多种方式之一。实际上,95%的函数都是纯洁的。
如果你没有严重的原因,不应该使用任何限制(比如不可变性)。如果你想“撤销”某个状态,你可以创建事务。如果你想简化通信,你可以发送带有不可变数据的事件。这取决于你。
我正在从后马克思主义共和国写这篇文章。我相信任何想法的激进化都是错误的方式。

第三段讲得非常有道理。谢谢你。“如果你想要“撤销”一些状态,你可以创建交易”!! - bozzmob
顺便说一下,面向对象编程(OOP)也可以与马克思主义进行比较。还记得Java吗?JavaScript中的奇怪的Java位?炒作从来不是好事,它会导致激进化和极端化。历史上,OOP比Facebook对Redux的炒作更加炒作。尽管他们确实尽了最大努力。 - ximo

4

另一种看法...

我的另一个回答从非常实际的角度回答了问题,我仍然喜欢它。我决定添加这个作为另一个答案,而不是那个答案的补充,因为这是一篇乏味的哲学漫谈,希望也回答了问题,但不太适合我的现有答案。

简短回答

即使在小型项目中,不变性也可能会有用,但不要假设它适用于您,因为它存在。

更长的回答

注意:为了本回答的目的,我使用“纪律”一词来表示出于某种好处而进行的自我否定。

这类似于另一个问题:“我应该使用TypeScript吗?JavaScript中类型很重要吗?”答案也类似。考虑以下情况:

你是一个JavaScript/CSS/HTML代码库的唯一作者和维护者,代码约5000行。你的半技术老板读到关于Typescript作为新热门技术的文章,并建议我们可能需要转移到Typescript,但决定权在你手中。所以你阅读相关文献并试用它等等。

现在你需要做出选择,你要转移到Typescript吗?

TypeScript具有一些引人注目的优点:智能感知、早期发现错误、预先指定API、易于在重构中修复问题、更少的测试等。但TypeScript也有一些成本:某些非常自然和正确的JavaScript语言习惯在它不是特别强大的类型系统中可能很棘手,注释增加了代码行数,重写现有代码库的时间和精力,构建流程中的额外步骤等等。更根本的是,它为实现正确的JavaScript程序划定了一个子集,以换取您的代码更可能是正确的承诺。它是任意地有限制性的,这正是重点所在:你施加一些纪律来限制自己(希望不会打自己的脚)。

回到问题本身,在上述段落的背景下重新表述问题:这值得吗?

在所描述的情况下,我认为如果您非常熟悉小型到中型JS代码库,则使用TypeScript的选择更多是美学上的考虑而不是实际上的考虑。这没关系,美学并没有错,只是并不一定令人信服。

情景B:

你换了工作,现在是Foo Corp的业务线程序员。你在一个10人团队中工作,负责一个约90000行(还在不断增加)的JavaScript/HTML/CSS代码库,涉及babel、webpack、一套polyfills、各种插件的react、一个状态管理系统、约20个第三方库、约10个内部库、类似于linter的编辑器插件,其中包括内部样式指南的规则等等。

回到你只写了5,000行代码时,这并不重要。甚至文档也不是那么重要,即使你在6个月后重新回到代码的某个部分,你也可以很容易地找到答案。但现在,遵循规范不仅仅是好习惯,而是必要的。这种规范可能不包括Typescript,但很可能会涉及到某种形式的静态分析,以及所有其他形式的编码规范(文档、样式指南、构建脚本、回归测试、CI)。遵循规范已经不再是一种奢侈,而是一种必需。

这一切都适用于1978年的GOTO:当你使用C语言开发一个小型的二十一点游戏时,你可以使用GOTO和意大利面条般的逻辑,但是随着程序变得越来越庞大和雄心勃勃,过度使用GOTO就无法持续下去了。今天所有这些都适用于不可变性。

就像静态类型一样,如果你不在一个由工程师维护/扩展的大型代码库上工作,使用不可变性基本上只是出于审美考虑:它的好处仍然存在,但可能还没有超过成本。

但就像所有有用的规范一样,总会到达一个不可避免的点。如果我想保持健康的体重,那么对冰淇淋的纪律可能是可选的。但如果我想成为一名竞技运动员,吃不吃冰淇淋的选择就要根据我的目标而定了。如果你想用软件改变世界,不可变性也许是你需要遵循的规范之一,以避免它在自己的重量下崩溃。


1
+1 我喜欢它。Jared 的观点更为精准。然而,不可变性并不能拯救一个团队缺乏纪律的问题。 - Steven de Salas
@StevendeSalas这是一种纪律形式。因此,我认为它与(但并不取代)其他形式的软件工程纪律相关。它是补充而非替代。但正如我在你的回答评论中所说,我一点也不惊讶,这正在被一个拥有一群工程师的科技巨头推动,他们都在同一个庞大的代码库上努力:) 他们需要尽可能多的纪律性。我大部分时间不会突变对象,但也不使用任何形式的强制执行,因为,嗯,只有我自己。 - Jared Smith

3

以一个例子来说明:

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

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

console.log(userMessage) // => ?

现在回答一些问题:

  1. mutable 代码中,sendMessageViaMobilePush(userMessage)) 的 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).  

  1. What is under userMessage on same line in immutable code?

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

  2. Can You guess by which method "id" is updated in mutable code in Snippet 1 ??

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

  3. In mutable code people didn't received push messages (sendMessageViaMobilePush). Can You guess why ??

    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中使用可变对象的主要问题。请注意,当复杂性增加时,如果与其他人一起工作,检查设置和位置将变得非常困难。

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