资讯专栏INFORMATION COLUMN

[译]JavaScript中的不可变性(Immutability)

clasnake / 1667人阅读

摘要:整个这个雷区面板都是由的和组成的,最后由的方法对其进行不可变化处理剩下的主要逻辑部分就是扫雷了,传入扫雷游戏对象一个不可变结构做为第一个参数,以及要扫的那个雷区块对象,最后返回新的扫雷游戏实例。

不可变性(Immutability)是函数式编程的核心原则,在面向对象编程里也有大量应用。在这篇文章里,我会给大家秀一下到底什么是不可变性(Immutability)、她为什么还这么屌、以及在JavaScript中怎么应用。

什么是不可变性(Immutability)?

还是先来看看关于可变性(Mutability)的教条式定义:“liable or subject to change or alteration(译者注:真他妈难翻,就简单理解成"易于改变的"吧)”。在编程领域里,我们用可变性(Mutability)来描述这样一种对象,它在创建之后状态依旧可被改变。那当我们说不可变(Immutable)时,就是可变(Mutable)的对立面了(译者注:原谅我翻的废话又多起来) - 意思是,创建之后,就再也不能被修改了。

如果我说的又让你感到诡异了,原谅我小小的提醒一下,其实我们平时使用的很多东西事实上都是不可变的哦!

var statement = "I am an immutable value";
var otherStr = statement.slice(8, 17);

我猜没人会吃惊,statement.slice(8, 17)并没有改变statement变量吧(译者注:如果你吃惊了,赶紧去补基本知识吧)?事实上,string对象上的所有方法里,没有一个会修改原string,它们一律返回新的string。原因简单了,因为string就是是不可变的(Immutable) - 它们不能被修改,我们能做的就是基于原string操作后得到一个新string

注意了,string可不是JavaScript里唯一内置的不可变(Immutable)数据类型哦。number也是不可变(Immutable)的。否则的话,你试想下这个表达式2 + 3,如果2的含义能被修改,那代码该怎么写啊|_|。听起来荒谬吧,但我们在编程中却常常对objectarray做出这种事儿。

JavaScript充满变化

JavaScript中,stringnumber从设计之初就是不可变(Immutable)的。但是,看看下面这个关于array例子:

var arr = [];
var v2 = arr.push(2);

来我问你,v2的值是什么?如果arraystringnumber一样也是不可变(Immutable)的,那此时v2必定是一个包含了一个数字2的新array。事实上,还真就不是那样的。这里arr引用的array被修改了,里面添了一个数字2,这时v2的值(也就是arr.push(2)的返回值),其实是arr此时的长度 - 就是1

试想我们拥有一个不可变的数组(ImmutableArray)。就像stringnumber那样,她应该能像如下这样被使用:

var arr = new ImmutableArray([1, 2, 3, 4]);
var v2 = arr.push(5);

arr.toArray(); // [1, 2, 3, 4]
v2.toArray();  // [1, 2, 3, 4, 5]

类似的,也可以有一个不可变的Map(ImmutableMap),理论上可以替代object应该于多数场景,她应该有一个set方法,不过这个set方法不会塞任何东西到原Map里,而是返回一个包含了塞入值的新Map

var person = new ImmutableMap({name: "Chris", age: 32});
var olderPerson = person.set("age", 33);

person.toObject(); // {name: "Chris", age: 32}
olderPerson.toObject(); // {name: "Chris", age: 33}

就像2 + 3这个表达式里,我们不可能改变2或是3所代表的含义,一个person在庆祝他33岁的生日,并不会影响他曾经是32岁的事实。

JavaScript不可变性(Immutability)实战

JavaScript里目前还没有不可变的listmap,所以暂时我们还是需要三方库的帮助。有两个很不错的,一个是Mori - 她把ClojureScript里持久化数据结构的API支持带到了JavaScript里;另一个是Facebook出品的immutable.js。后面的示例里,我将使用immutable.js,因为她的API对于JavaScript开发者更友好一些。

下面的例子里,我们使用不可变(Immutable)知识来构建一个扫雷小游戏。扫雷的游戏面板我们用一个不可变的map来构建,其中tiles(雷区区块)部分值得关注哦,它是一个由不可变map组成的不可变list(译者注:又开始绕了),其中每一个不可变的map表示一个tile(雷区块)。整个这个雷区面板都是由JavaScriptobjectarray组成的,最后由immutable.js的fromJS方法对其进行不可变化处理:

function createGame(options) {
  return Immutable.fromJS({
    cols: options.cols,
    rows: options.rows,
    tiles: initTiles(options.rows, options.cols, options.mines)
  });
}

剩下的主要逻辑部分就是“扫雷”了,传入扫雷游戏对象(一个不可变结构)做为第一个参数,以及要“扫”的那个tile(雷区块)对象,最后返回新的扫雷游戏实例。以下我们就要讲到这个revealTile函数。当它被调用时,tile(雷区块)的状态就要被重置为“扫过”的状态。如果是可变编程,代码很简单:

function revealTile(game, tile) {
  game.tiles[tile].isRevealed = true;
}

然后再来看看如果用上面介绍的不可变数据结构来编码,坦白讲,一开始代码变得都点丑了:

function revealTile(game, tile) {
  var updatedTile = game.get("tiles").get(tile).set("isRevealed", true);
  var updatedTiles = game.get("tiles").set(tile, updatedTile);
  return game.set("tiles", updatedTiles);
}

我去,丑爆了有木有!

万幸,不可变性不止于此,一定有得救!这种需求很常见,所以工具早就考虑到了,可以这么操作:

function revealTile(game, tile) {
  return game.setIn(["tiles", tile, "isRevealed"], true);
}

现在revealTile返回一个新的实例了,新实例里其中一个tile(雷区块)的isRevealed就和之前那个game实例里的不一样了。这里面用到的setIn是一个null-safe(空值安全)的函数,任意keyPath中的key不存在时,都会在这个位置创建一个新的不可变map(译者注:这句略绕,个人认为既然这里不是主讲immutable.js,那就没必要非提一下它的这个特性,反而不清不楚,原作没细说,那我也就不多说了,有兴趣的可以来这里自己揣摩)。这个null-safe特性对于我们现在扫雷游戏这个例子并不合适,因为“扫”一个不存在的tile(雷区块)表示我们正在试图扫雷区以外的地方,那显然不对!这里需要多做一步检查,通过getIn方法检查tile(雷区块)是否存在,然后再“扫”它:

function revealTile(game, tile) {
  return game.getIn(["tiles", tile]) ?
    game.setIn(["tiles", tile, "isRevealed"], true) :
    game;
}

如果tile(雷区块)不存在,我们就返回原扫雷游戏实例。这就是个可迅速上手的关于不可变性(Immutability)的练习,想深入了解的可以看codepen,完整的实现都在里面了。

Performance怎么样?

你可能觉得,这他妈Performance应该low爆了吧,我只能说某些情况下你是对的。每当你想添加点东西到一个不可变(Immutable)对象里时,她一定是先拷贝以存在值到新实例里,然后再给新实例添加内容,最后返回新实例。相比可变对象,这势必会有更多内存、计算量消耗。

因为不可变(Immutable)对象永远不变,实际上有一种实现策略叫“结构共享”,使得她的内存消耗远比你想象的少。虽然和内置的arrayobject的“变化”相比仍然会有额外的开销,但这个开始恒定,绝对可以被不可变性(Immutability)带来的其它众多优势所消磨、减少。在实践中,不可变性(Immutability)带来的优势可以极大的优化程序的整体性能,即使其中的某些个别操作开销变大了。

改进变更追踪

各种UI框架里,最难的部分永远是变更追踪(译者注:或者叫“脏检查”)。这是JavaScript社区里的普遍问题,所以EcmaScript 7里提供了多带带的API在保证Performance的前提下可以追踪变化:Object.observe()。很多人为之激动,但也有不少人认为这个API然并卵。他们认为,在任何情况下,这个API都没很好的解决变更追踪问题:

var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}];
Object.observe(tiles, function () { /* ... */ });

tiles[0].id = 2;

上面例子里,tiles[0]的变更并没有触发observer,所以其实这个提案即便是最简单的变更追踪也没做到。那不可变性(Immutability)又是怎么解决的?假设有一个应用状态a,然后它内部有值被改变了,于是就得到了一个新的实例b

if (a === b) {
  // 数据没变,停止操作
}

如果应用状态a没有被修改,那b就是a,它们指向同一个实例,===就够了,不用做其他事儿。当然这需要我们追踪应用状态的引用,但整个问题的复杂度被大大简化了,现在只要判断一下它们是否同一个实例的引用就好了,真心不用再去深入调查里面的某某字段是不是变了。

结束语

希望本文能某种程度上帮你了解不可变性(Immutability)是如何帮我们优化/改进代码的,也希望这些例子从实践角度说清楚了使用方式。不可变性(Immutability)的热度在持续增高,我确定这绝不是你今年看到的关于不可变性(Immutability)的最后一文。同志们,是时候来一发了,我相信你用过后一定会high至的,就像我现在一样^^。

原文地址:Immutability in JavaScript

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79167.html

相关文章

  • [] 理解 JavaScript Mutation 突变和 PureFunction 纯函数

    摘要:突变引起状态的改变。纯函数和副作用纯函数是接受输入并返回值而不修改其范围之外的任何数据的函数副作用。如果它们不同,则调用函数,以更新新状态。 showImg(https://segmentfault.com/img/remote/1460000018524546?w=1280&h=834); 作者:Chidume Nnamdi 英文原文:https://blog.bitsrc.io/...

    Enlightenment 评论0 收藏0
  • 2017-09-30 前端日报

    摘要:前端日报精选刘海打理指北中的错误处理模式与反模式译图解和译你并不知道中文装饰器让你的代码更简洁众成翻译第期每个程序员第一份工作前应该知道的件事中的不变性众成翻译写的一次小结掘金内部机制探秘和文末附彩蛋和源码前端杂谈开发实战 2017-09-30 前端日报 精选 iPhone X 刘海打理指北React16中的错误处理ES6 Promise:模式与反模式「译」图解 ArrayBuffer...

    darryrzhong 评论0 收藏0
  • 学会使用函数式编程的程序员(第1部分)

    摘要:函数式编程的目标是尽量写更多的纯函数,并将其与程序的其他部分隔离开来。在函数式编程中,是非法的。函数式编程使用参数保存状态,最好的例子就是递归。函数式编程使用递归进行循环。在函数式编程中,函数是一级公民。 showImg(https://segmentfault.com/img/bVblxCO?w=1600&h=710); 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等...

    Steven 评论0 收藏0
  • JavaScript中的Object.freeze与const之间的区别()

    摘要:一些开发人员特别是新手们会认为这两个功能的工作方式是一样的,但其实并不是。的问题使用声明的对象仅能阻止其重新分配,但是并不能使其声明的对象具有不可变性能够阻止更改其属性。因此,当具有嵌套属性的对象时,并不能完全冻结对象。 原文:The differences between Object.freeze() vs Const in JavaScript 作者:Bolaji Ayodeji...

    monw3c 评论0 收藏0
  • Java 并发设计模式

    摘要:并发设计模式一模式的使用表示线程本地存储模式。为不同的任务创建不同的线程池,这样能够有效的避免死锁问题。两阶段终止,即将线程的结束分为了两个阶段,第一个阶段是一个线程向另一个线程发送终止指令,第二个阶段是线程响应终止指令。 Java 并发设计模式 一、Thread Local Storage 模式 1. ThreadLocal 的使用 Thread Local Storage 表示线程...

    zero 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<