当前位置: 首页 > Web前端 > JavaScript

JavaScript中的可变性和不可变性

时间:2023-03-27 13:45:44 JavaScript

不可变性(Immutability)是函数式编程的核心原则,在面向对象编程中也被广泛使用。在这篇文章中,我将向您展示什么是不可变性,为什么它如此酷,以及它是如何在JavaScript中应用的。什么是不变性?我们来看看Mutability的教条式定义:“liableorsubjecttochangeoralteration(译者注:翻译起来真他妈的难,我们就简单理解为‘easytochange’吧)”。在编程中,我们用Mutability来描述一个对象,它的状态在创建后仍然可以改变。那么当我们说不可变(Immutable)的时候,它是和Mutable相反的(译者注:原谅我又开始胡说八道了)——意思是,创建之后,就不能再修改了。如果我说的又让你觉得怪怪的,请原谅我的一个小提醒,其实很多我们平时用到的东西其实是不可变的!varstatement='我是一个不可变的值';varotherStr=statement.slice(8,17);statement.slice(8,17)没有改变statement变量我估计没有人会感到惊讶(译者注:如果你感到惊讶,赶快把基础知识补上吧)?事实上,字符串对象上的所有方法都不会修改原来的字符串,它们都返回一个新的字符串。原因很简单,因为字符串是不可变的(Immutable)——它们不能被修改,我们能做的就是在原有字符串的基础上操作得到一个新的字符串。请注意,字符串并不是JavaScript中唯一的内置不可变数据类型。number也是不可变的(Immutable)。不然就想象一下表达式2+3,如果2的意思可以修改,那代码应该怎么写|_|。听起来很可笑,但是我们在编程中经常对对象和数组做这种事情。JavaScript充满变化在JavaScript中,字符串和数字从设计之初就是不可变的(Immutable)。但是,请看下面关于数组的示例:vararr=[];varv2=arr.push(2);我问你,v2的值是多少?如果array也像string和number一样是不可变的(Immutable),那么此时v2一定是一个包含数字2的新数组。事实上,它真的不是那样的。这里修改了arr引用的数组,在里面加了一个数字2。这时候v2的值(也就是arr.push(2)的返回值)其实就是此时arr的长度——也就是1。假设我们有一个不可变数组(ImmutableArray)。像字符串和数字一样,她应该可以这样使用:vararr=newImmutableArray([1,2,3,4]);varv2=arr.push(5);arr.toArray();//[1,2,3,4]v2.toArray();//类似于[1,2,3,4,5],也可以有一个不可变的Map(ImmutableMap),理论上可以代替对象should大多数场景下,她应该有一个set方法,但是这个set方法会不在原始Map中插入任何内容,而是返回包含插入值的新Map:varperson=newImmutableMap({name:'Chris',age:32});varolderPerson=person.set('age',33);person.toObject();//{name:'Chris',age:32}olderPerson.toObject();//{name:'Chris',age:33}就像表达式2+3一样,我们不可能改变2或3的含义。一个人庆祝他的33岁生日不会影响他32岁旧的事实。JavaScript不可变性(Immutability)实战JavaScript目前没有不可变的列表和映射,所以我们暂时还是需要第三方库的帮助。有两个非常好的,一个是Mori——她把ClojureScript中持久化数据结构的API支持带到了JavaScript中;另一个是Facebook出品的immutable.js。在下面的示例中,我将使用immutable.js,因为它的API对JavaScript开发人员更友好。在下面的例子中,我们使用不变的知识来构建一个扫雷游戏。我们使用不可变地图来构建扫雷游戏面板。瓷砖(雷区块)部分值得注意。它是由一个不可变地图(译者注:又开始绕圈了)组成的一个不可变列表,其中每个不可变地图代表一个tile(矿块)。整个雷区面板由JavaScript对象和数组组成,最后immutable.js的fromJS方法让它不可变:(options.rows,options.cols,options.mines)});}剩下的主要逻辑部分是“扫雷”,扫雷游戏对象(一个不可变结构)作为第一个参数传入,瓦片(mineblock)对象被“扫除”,最后返回一个新的扫雷游戏实例。接下来我们就来说说这个revealTile函数。当它被调用时,瓦片(地雷块)的状态将被重置为“扫过”状态。如果是变量编程,代码很简单:functionrevealTile(game,tile){game.tiles[tile].isRevealed=true;}然后我们看看是不是用上面介绍的不可变数据结构来编码,坦白说,a开头的代码有点难看:functionrevealTile(game,tile){varupdatedTile=game.get('tiles').get(tile).set('isRevealed',true);varupdatedTiles=game.get('tiles').set(tile,updatedTile);returngame.set('tiles',updatedTiles);}我去,丑死了!幸好不变性不止于此,必有救赎!这种需求很常见,所以工具已经考虑到了,你可以这样做:functionrevealTile(game,tile){returngame.setIn(['tiles',tile,'isRevealed'],true);}现在revealTile返回了一个新实例,新实例中其中一个瓦片的isRevealed与之前游戏实例中的不同。这里使用的setIn是一个null-safe(空值安全)函数。当keyPath中的任意key不存在时,会在该位置创建一个新的immutablemap(译者注:这句话略有绕口,个人认为既然immutable.js不是这里的主要话题,就没必要提到了它的特点,但不是很清楚,原著也没有详述,就不多说了,有兴趣的可以来这里自己琢磨)。这种空安全特性不适合我们当前的扫雷游戏示例,因为“扫”一个不存在的瓦片(地雷块)意味着我们正在尝试清除雷区之外的地方,这显然是错误的!这里我们需要做一个额外的检查,通过getIn方法检查tile(矿块)是否存在,然后“扫”它:functionrevealTile(game,tile){returngame.getIn(['tiles',tile])?game.setIn(['tiles',tile,'isRevealed'],true):game;}如果瓦片(地雷块)不存在,我们返回到原始的扫雷游戏实例。这是一个关于不可变性的练习,您可以快速上手。如果想了解更多,可以看codepen,点击预览。完整的实现在里面。性能如何?你可能认为这个他妈的Performance应该很低,但我只能说在某些情况下你是对的。每当你要向一个不可变(Immutable)对象中添加一些东西时,她必须先将已有的值复制到新的实例中,然后再向新的实例中添加内容,最后返回新的实例。与可变对象相比,这必然会消耗更多的内存和计算量。因为不可变(Immutable)对象永远不会改变,所以其实有一种实现策略叫做“结构共享”,这使得她的内存消耗远没有你想象的那么大。虽然与内置数组和对象的“变化”相比,还是会有额外的开销,但这个一开始是常量,绝对可以被不可变性带来的其他诸多优势所消耗和减少。在实践中,不变性带来的优势可以极大地优化程序的整体性能,即使某些个别操作的开销变大了。改进变更跟踪在各种UI框架中,最难的部分始终是变更跟踪(译者注:或称为“脏检查”)。这是JavaScript社区的通病,因此EcmaScript7提供了一个单独的API,可以在保证性能的前提下跟踪变化:Object.observe()。许多人对此感到兴奋,但也有许多人认为这个API没有用。他们争辩说,无论如何,这个API都不能很好地解决更改跟踪问题:vartiles=[{id:0,isRevealed:false},{id:1,isRevealed:true}];Object.observe(tiles,function(){/*...*/});瓷砖[0].id=2;在上面的例子中,tiles[0]的变化并没有触发观察者,所以这个proposal其实是最简单的changetrackingNeitherdid。不变性是如何解决的?假设有一个应用状态a,然后它里面的一个值被改变了,所以得到一个新的实例b:if(a===b){//数据没有改变,停止运行}如果应用状态a没有被改变修改,那么b就是a,它们指向同一个实例,===就够了,不需要做其他的事情。当然,这需要我们跟踪应用状态的引用,但是整个问题的复杂度已经大大简化了。现在我们只需要判断它们是否是对同一个实例的引用即可。里面的某个字段是否发生了变化,我真的不需要深究。向上。