JavaScript中的不变性

不变性是函数式编程中的核心原则,在很多面向对象程序中也都有所体现。在这篇文章中,我将精确地说明什么是不变性、如何在JavaScript中使用这个概念以及为什么它是有用的。

什么是不变性?

可变性的文本定义是“易于改变或变化的”。在编程中,我们使用这个词来表示允状态随时间而变化的对象。一个不可改变的值的定义是完全相反的——在被创建之后,它永远不会改变。

如果你觉得这看起来很奇怪,请允许我提醒你:我们一直使用的许多值实际上是不可改变的。

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

我相信没有人会惊讶地发现第二行决不会改变statement中的字符串。实际上,没有字符串方法可以改变他们操作的字符串,它们都是返回新的字符串。原因是字符串是不可变的——它们不能改变,我们只能创建新的字符串。

字符串并不是JavaScript内置的唯一不变的值。数字也是不变的。你能想象一个计算表达式2+3改变了数字2的含义吗?
但是我们一直在用我们的对象和数组做这样的事情,虽然这听起来很荒谬。

在JavaScript中,变化是大量存在的

在JavaScript中,字符串和数字是特意被设计成不可变的。但是,请考虑以下使用数组的示例:

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

v2有什么值?如果数组与字符串和数字一样,v2将包含一个新数组,新数组中包含一个元素——数字2。然而,事实并非如此。相反,arr引用已被更新为包含数字2,而v2则包含了arr的新长度。

想象一下ImmutableArray类型。受字符串和数字特性的启发,它会有以下特性:

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]

类似地,可以替代大多数对象的ImmutableMap将具有“设置”属性的方法,实际上该方法不会设置任何内容,但是返回具有所需更改内容的新对象:

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的意义一样,一个庆祝他们33岁生日的人不会改变他们曾经是32岁的事实。

不变性在JavaScript中的实践

JavaScript现在还没有不可变的list和map,所以我们现在需要一个第三方库。这里有两个非常优秀的库我们可以使用。第一个是Mori,它可以确保在JavaScript中使用ClojureScript的持久数据结构和JavaScript支持的API。另一个是由Facebook开发人员撰写的immutable.js。对于这个演示我会使用immutable.js,仅仅因为JavaScript开发人员更熟悉它的API。

在这个演示中,我们将看一看扫雷游戏中的不可变数据是如何工作的。扫雷区域的板块由一个不可变的map表示,其中最有趣的数据部分是tiles。它是一个由不可变maps组成的不可变list,其中每个map代表了板块上的一个tile区域。整个东西是使用JavaScript对象和数组初始化的,然后使用immutable.js的fromjs函数使其永生化。

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

游戏核心逻辑的剩余部分是被实现成了一个函数,这个函数将这个不可变结构作为第一个参数,并且返回一个新的实例。最重要的函数是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与以前的版本不同。setIn是空安全的,如果键中不存在任何内容,将使用空对象填充。对于这个游戏来说这是不可取的,因为一块丢失的tile表示我们试图在扫雷板块外面露出一块tile。这里可以在操作之前通过使用getIn函数查找tile来解决这种问题:

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

如果tile不存在,我们只需返回现有的游戏。这个演示是一个在实践中对不变性的快速体验,想更深入了解的人可以看看这个CodePen,其中包括一个完整的实现扫雷游戏的规则。

性能如何?

你可能会认为这会产生可怕的性能效果,在某些方面你是正确的。你无论何时向不可变对象添加内容,都需要通过复制现有值并将新值添加到它来创建新实例。这必将比转换单个对象更占用密集的内存以及处理更具挑战性的计算。

因为不可变的对象永远不会改变,它们可以使用一种称为“结构共享”的策略来实现,这将花费比你预期的要少得多的内存开销。与内置数组和对象相比,它仍然会有一个开销,但它是不变的,通常还可以通过不变性提供的其他优势来缩小。在实践中,许多情况下不可变数据的使用将提高你的应用程序的整体性能,即使某些孤立的操作将变得更加困难。

改进的更改跟踪

在任何UI框架中最困难的工作之一就是变化跟踪。人们对ECMAScript7提供了一个单独的API:Object.observe(),以帮助跟踪具有更好性能的目标变化存在广泛的质疑。虽然很多人都对这个API感到兴奋,但其他人觉得这是不正确的。在任何情况下,它都不能正确地解决更改跟踪问题:

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

tiles[0].id = 2;

tiles[0]对象的更改不会触发我们的更改观察事件,因此,提出的更改跟踪机制几乎失败甚至是最微不足道的案例。在这种情况下,不变性将怎样提供帮助?给定应用程序状态a和潜在的新应用程序状态b:

if (a === b) {
  // 数据没有改变,中止
}

如果应用程序状态尚未被更新,那么它将与以前一样,我们根本不需要做任何事情。这确实要求我们跟踪持有该状态的引用,但现在整个问题已经简化到管理单个引用。

结论

我希望这篇文章能给你一些关于不变性是如何帮助你改进你的代码的底层知识,所提供的例子可以说明这样的做法在实践中是如何实现的。不变性的重要性在不断提升,这不会是你今年阅读的关于这个问题的最后一篇文章。因为我没有时间,你可以多去尝试这个事情,我保证你对于它会很兴奋。

感谢Christian先生允许我翻译该文章,原文链接:https://www.sitepoint.com/immutability-javascript/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容