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

我目前正在研究JS的反应反应本地框架。在中途,当我读到Facebook的Flux和Redux实现时,我遇到了Immutability或Immutable-JS图书馆

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

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

如果我设置一个值为最初对象数组,我不能操作它。这就是不可变原理,对吧?(如果我说错了请指正。) 但是,如果我有一个新的News对象需要更新怎么办?通常情况下,我可以将对象添加到数组中。 在这种情况下我该如何实现呢?删除存储并重新创建它? 向数组中添加对象难道不是一种成本较低的操作吗?< / p >

65130 次浏览

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

事实上,情况恰恰相反:至少从长远来看,可变性会让事情变得更复杂。是的,它使你的初始编码更容易,因为你可以在任何你想要的地方修改东西,但当你的程序变大时,它就变成了一个问题——如果一个值改变了,是什么改变了它?

当你让所有东西都是不可变的,这意味着数据不能再被意外改变了。你肯定知道,如果你把一个值传递给一个函数,它就不能在那个函数中被改变。

简单地说:如果你使用不可变的值,它会让你的代码变得非常容易:每个人都有一个唯一的数据副本,所以它不会破坏它,破坏你代码的其他部分。想象一下,这使得在多线程环境中工作变得多么容易!

注1:不变性有潜在的性能成本,这取决于你在做什么,但像Immutable.js这样的东西会尽其所能优化。

注2:在你不确定的情况下,Immutable.js和ES6 const意味着非常不同的东西。

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

是的,你的新闻例子非常好,你的推理也非常正确:你不能只是修改现有的列表,所以你需要创建一个新的列表:

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

我最近也在研究同样的话题。我会尽我最大的努力回答你的问题,并尝试分享我到目前为止学到的东西。

问题是,为什么不可变性如此重要?有什么问题 变异的对象?这不是让事情变得简单了吗?< / p >

基本上可以归结为这样一个事实:不可变性增加了可预测性、性能(间接地),并允许突变跟踪。

可预测性

突变隐藏了更改,而更改会产生(意想不到的)副作用,从而导致严重的bug。当您强制执行不可变性时,您可以保持应用程序架构和心理模型的简单性,这使您更容易对应用程序进行推理。

性能

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

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

.分配小对象

你可以阅读更多关于在这里

突变跟踪

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

额外的资源:

如果我设置一个带有初始值的对象数组。我不能 对它进行操作。这就是不可变原理,对吧?(正确的 如果我错了,请告诉我)。但是,如果我有一个新的News对象 被更新吗?通常情况下,我可以将对象添加到 数组中。在这种情况下我该如何实现呢?删除存储&重新创建吗? 向数组中添加对象难道不是一种成本较低的操作吗?< / p >

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

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

egghead.io上有一个很棒的redux课程,其中丹•阿布拉莫夫, redux的作者,解释了这些原则如下(我修改了一些代码,以更好地适应场景):

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

此外,这些视频进一步详细演示了如何实现不变性:

  • < a href = " https://egghead。io /经验/ javascript-redux-avoiding-array-mutations-with-concat-slice-and-spread noreferrer“rel = >阵列< / >
  • < a href = " https://egghead。io /经验/ javascript-redux-avoiding-object-mutations-with-object-assign-and-spread noreferrer“rel = > < / >对象

虽然其他答案都很好,但为了解决你关于实际用例的问题(来自其他答案的评论),让我们暂时走出你的运行代码,看看你鼻子底下无处不在的答案:git。如果每次push commit时覆盖了存储库中的数据会发生什么?

现在我们遇到了不可变集合面临的一个问题:内存膨胀。Git足够聪明,不会在每次更改它只是记录了微分时简单地生成新的文件副本。

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

这种策略对于内存中的数据结构也是合理的性能,因为有著名的树操作算法在对数时间内运行。

另一个用例:假设你想在你的webapp上有一个撤销按钮。对于数据的不可变表示,实现这一点相对简单。但是如果您依赖于突变,这意味着您必须担心缓存世界的状态并进行原子更新。

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

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

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

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

对于UI,我们需要使它可预测的。 如果UI = f(application state).

. 0,它将是可预测的

应用程序(在JavaScript中)通过使用减速机功能实现的操作来改变状态。

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

new state  = r(current state, action)

enter image description here

这样做的好处是:你可以对状态进行时间旅行,因为所有的状态对象都被保存了,而且你可以在UI = f(state)以来的任何状态下呈现应用程序

所以你可以很容易地撤销/重做。


碰巧创建所有这些状态仍然可以是内存高效的,与Git类似是很好的,我们在Linux操作系统中有类似的符号链接(基于索引节点)。

不变性的逆向观点

TL/DR:不可变性在JavaScript中更像是一种时尚趋势,而不是必须。如果你正在使用React,它确实在状态管理中为一些令人困惑的设计选择提供了一个整洁的变通方法。然而,在大多数其他情况下,它不会在它引入的复杂性上增加足够的价值,更多地服务于填满简历而不是满足实际的客户端需求。

长话短说:阅读下文。

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

很高兴你问了这个问题!

前段时间,一个非常有才华的家伙丹•阿布拉莫夫写了一个javascript状态管理库回来的,它使用纯函数和不可变性。他还做了一些非常酷的视频,使这个想法真的很容易理解(和销售)。

时机很好。的新鲜感正在消退,JavaScript世界已经准备好关注最新的东西,它有适当的酷的程度,这个库不仅创新,而且与另一个硅谷强国兜售的反应完美地结合在一起。

很遗憾,JavaScript的世界是由时尚主宰的。现在阿布拉莫夫被誉为半神,我们所有的凡人都必须服从自己的不变之道…不管它是否有意义。

改变对象有什么错?

没有什么!

事实上,程序员一直在改变对象,呃…只要有对象就会变异。换句话说,应用程序开发的50年以上

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

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

是的!.. 当然了!

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

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

好吧,你可以采用传统的方法并更新News对象,这样你在内存中对该对象的表示就会改变(以及显示给用户的视图,或者人们希望如此)…

或者……

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

我想知道这里是什么。请启发我:)

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

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

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

1)不可变性在多线程环境中避免竞争条件非常棒

多线程环境(如c++, Java和c#)在多个线程想要更改对象时,会锁定对象。这对性能不利,但比数据损坏要好。然而,这并不像让一切都是不变的那样好(上帝赞美Haskell!)

但是唉!在JavaScript中你总是在单个线程上操作。甚至是web worker(每个都在独立的环境中运行)。因此,既然你不能在你的执行上下文中有线程相关的竞态条件(所有那些可爱的全局变量和闭包),支持不可变的主要观点就被抛弃了。

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

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

这里是问题的真正关键,大多数(React)开发者会告诉你不可变性和FP可以以某种方式创造这个魔法,让你的应用程序的状态变得可预测。

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

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

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

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

3)比赛条件非常糟糕。

如果你在用React的话,它们可能是。但如果你选择不同的框架,它们就很少出现。

此外,你通常有更大的问题来处理依赖地狱之类的问题。比如一个臃肿的代码库。比如你的CSS没有被加载。比如缓慢的构建过程,或者被固定在一个单一的后端,使得迭代几乎不可能。就像没有经验的开发者不明白发生了什么,把事情搞得一团糟。

你知道的。现实。但是,谁会在乎这些呢?

4)不可变性使用引用类型以减少跟踪每个状态更改的性能影响。

因为说真的,如果你打算在每次状态改变时复制东西,你最好确保你在这方面是聪明的。

5)不可变性允许你撤销某些内容

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

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

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

一旦发现这个概念(状态是一连串的事件——而不是代表最新观点的一组粗糙的记录),不变的世界就变成了一个神奇的地方。这是一片充满奇迹和可能性的土地。如果做得正确,这绝对可以使实时应用程序更容易完成,你只需要将事件流广播给每个感兴趣的人,这样他们就可以将当前的事件流建立自己的代表,并将自己的更改写回公共流中。

但在某些时候你醒来,意识到所有的奇迹和魔法不免费来吗。与你那些热切的同事不同,你的利益相关者(是的,付钱给你的人)并不关心哲学或时尚,而是非常关心他们为开发一款可以销售的产品所付出的钱。最重要的是,为不可变性编写代码更困难,而打破它更容易,此外,如果没有后端来支持它,那么拥有一个不可变的前端就没有什么意义了。当(如果!)你最终说服你的利益相关者,你应该通过像WebSockets一样的推动技术来发布和消费事件时,你会发现大规模生产是痛苦的


现在给你一些建议,如果你选择接受的话。

选择使用FP/Immutability编写JavaScript也会使应用程序代码库更大、更复杂、更难管理。我强烈主张将这种方法限制在Redux减速器上,除非你知道自己在做什么……如果你打算继续使用不可变性,那么应用整个应用程序堆栈的不可变状态,而不仅仅是客户端。毕竟,有一个不可变的前端,然后把它连接到一个所有记录都有一个单一可变版本的数据库,这没什么意义。你只是回到了你试图逃避的问题上!

现在,如果你足够幸运能够在你的工作中做出选择,那么试着使用你的智慧(或不)和做对付钱给你的人正确的事。你可以根据你的经验,你的直觉,或者你周围发生的事情(诚然,如果每个人都在使用React/Redux,那么就有一个有效的论点,这将更容易找到一个资源来继续你的工作)。或者,你可以尝试简历驱动开发炒作驱动的开发方法。他们可能更适合你。

简而言之,关于不变性的说法是,它使你在同龄人中变得时髦,至少在下一次热潮到来之前,到那时你会很高兴地继续前进。


现在,在这段自我治疗之后,我想指出的是,我已经在我的博客中添加了这篇文章=>JavaScript中的不可变性:逆向观点。如果你有强烈的感觉,也想倾诉,请在那里回复;)

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

假设我们有一个名为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始终处于有效状态。

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

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

关于可变性

从技术角度来看,可变性并没有错。它是快速的,它是重复使用内存。开发人员从一开始就习惯了它(我记得)。可变性的使用存在问题,也会带来一些麻烦。

如果object不与任何东西共享,例如存在于函数的作用域中并且不对外公开,那么就很难看到不变性的好处。在这种情况下,不可变是没有意义的。一成不变的感觉始于某些东西被分享。

可变性头痛

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

其次经常与突变的问题是损坏的状态。当突变过程中途失败,一些字段被修改,一些字段没有被修改时,就会发生损坏状态。

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

最后,突变是信任缺失的原因。如果某个结构可以变异,你怎么能确定它有你想要的价值。

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

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

不可变性与价值有关

不可变性意味着改变不是在相同的对象、结构上完成的,而是在新的对象、结构中表示的。这是因为引用不仅表示内存指针的值。每一次改变都会创造新的价值,而不会改变旧的价值。这样清晰的规则给予了信任和代码的可预测性。函数使用起来是安全的,因为它们处理的不是突变,而是具有自己值的自己的版本。

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

不可变结构表示值。

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

从前,线程之间的数据同步出现了一个问题。这个问题非常麻烦,有10多个解决方案。有些人试图从根本上解决这个问题。它是函数式编程诞生的地方。这就像马克思主义。我不明白Dan Abramov是怎么把这个想法卖给JS的,因为它是单线程的。他是个天才。

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

如果你真的没有明确的理由,你就不应该使用任何限制(比如不可变性)。如果你想“撤销”某些状态,你可以创建事务。如果你想简化通信,你可以发送带有不可变数据的事件。这取决于你。

我写这条信息来自后马克思主义共和国。我相信任何思想的激进化都是错误的。

不同的看法……

我的另一个答案是从非常实际的角度来解决这个问题的,我仍然喜欢它。我决定把这个作为另一个答案,而不是对那个答案的补充,因为它是一个无聊的哲学咆哮,希望它也回答了这个问题,但并不真正符合我现有的答案。

博士TL;

即使在小型项目中,不变性也很有用,但不要认为它的存在就是为你准备的。

答案要长得多

注意:为了这个回答的目的,我使用“纪律”这个词来表示为了一些好处而自我否定。

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

你是5000行JavaScript/CSS/HTML代码库的唯一作者和维护者。你半技术性的老板读了一些关于typescript的最新热点,并建议我们可能想要改用它,但把决定留给你。所以你读到它,玩它,等等。

现在你要做一个选择,你是转到Typescript?

Typescript有一些引人注目的优点:智能感知,及早捕捉错误,提前指定api,在重构时容易修复错误,测试更少。Typescript也有一些成本:在它不是特别强大的类型系统中,某些非常自然和正确的JavaScript习惯可能很难建模,注释增加了LoC,重写现有代码库的时间和精力,构建管道中的额外步骤,等等。更根本的是,它为可能正确的JavaScript程序雕刻出子集,以换取你的代码是正确的更有可能的承诺。它是任意限制的。这就是关键所在:你强加一些约束自己的纪律(最好不要搬起石头砸自己的脚)。

回到问题上,在上一段的上下文中重新措辞:它是值得的吗?

在上述场景中,我认为,如果您非常熟悉中小型JS代码库,那么选择使用Typescript更具有美感而非实用性。这是,没有任何与美学有关的错误的,它们只是不一定引人注目。

场景2:

你换了工作,现在是Foo公司的一名业务线程序员。你和一个10人团队一起工作,开发90000个LoC(还在不断增加)JavaScript/HTML/CSS代码库,以及一个相当复杂的构建管道,包括babel、webpack、一套polyfills、react与各种插件、一个状态管理系统、大约20个第三方库、大约10个内部库、编辑器插件(如带有内部风格指南规则的linter)等等。

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

所有这些都适用于1978年的GOTO:你的C语言的小21点游戏可以使用GOTO和意大利面条逻辑,选择你自己的冒险方式并不是什么大不了的事情,但随着程序变得越来越大,越来越有野心,嗯,没有纪律的GOTO的使用就不能持续了。所有这些都适用于今天的不变性。

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

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

我已经为可变(或不可变)状态创建了一个框架不可知的开源(MIT)库,它可以取代所有那些不可变的存储,如库(redux, vuex等…)

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

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

使用deep-state-observer,我可以微调事情,我可以对组件行为进行粒度控制,因此性能可以大大提高。代码可读性更强,重构也更容易——只需要搜索和替换路径字符串(不需要更改代码/逻辑)。

举个例子:

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


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


console.log(userMessage) // => ?

现在回答一些问题:

  1. 可变的代码中的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. 不可变的代码的同一行中userMessage下面是什么?

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

  2. 你能猜出用哪种方法"id"在片段1中的可变的代码中更新??

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

  3. 可变的代码中,人们没有收到推送消息(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中使用可变对象的主要问题。

请注意,当复杂性上升时,很难检查设置的内容和位置,特别是当您与他人一起工作时。

不变性的主要优点是简单。

替换对象比修改现有对象简单。

它让你专注于一个地方的正确性。而不是每一个可能发生变化的地方。

如果你的对象处于无效状态,它更容易修复,因为错误必须发生在你创建它的时候(因为它是不可变的)