JavaScript 中的 不变性(Immutability)

原文出处 Immutability in JavaScript — SitePoint

不变性(Immutability)是函数式编程的核心原理,也有很多面向对象的程序提供了这一特性。在这篇文章中,我将展示什么是完全不变的,如何在JavaScript中使用这个概念,以及为什么它是有用的。

什么是不变性?

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

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

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

我认为不会有人惊讶,第二行并没有改变“statement”中的字符串。 实际上,String 类定义的方法都不能改变字符串的内容,它们都返回新的字符串。 原因是字符串是不可变的 - 它们不能改变,我们只能创建新的字符串。

字符串不是JavaScript内置的唯一不变的值。 数字也是不变的。 你甚至可以想象一个评估表达式“2 + 3”_改变数字“2”的含义的环境? 这听起来很荒唐,但是我们一直在使用对象和数组。

In JavaScript, Mutability Abounds

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

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

v2的值是多少?如果数组与字符串和数字的处理一致,v2将包含一个新数组,其中包含一个元素 - 数字2 - 。然而,这种情况并非如此。相反,arr引用已被更新为包含数字,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”类型可以替代大多数对象, 提供“set”方法,不给原有对象设置任何内容的属性,但返回具有所需更改的新对象:

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还没有不可变的列表和地图,所以我们现在需要一个第三方库。有两个很好的可用。第一个是[Mori](https://github.com/swannodette/mori),它可以在JavaScript中使用ClojureScript的持久数据结构和支持的API。另一个是由Facebook开发人员撰写的[immutable.js](https://github.com/facebook/immutable-js)。对于这个演示,我将使用immutable.js,因为它的API对JavaScript开发人员更熟悉。

对于这次演示,我们将用不可变数据介绍“扫雷”游戏是如何工作的。该board由不可变的map表示,最有趣的数据是“tiles”。这是一个不可变的map列表,其中每个map都代表board的tiles。整个事情都是使用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是空安全的,如果的任何部分不存在,它将填充空对象。在扫雷板的情况下,这是不可取的,因为缺少的瓦片意味着我们试图在板外显示瓦片。这可以通过使用“getIn”在操作之前查找它来缓解:

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

如果tile不存在,我们只需返回现有的游戏。这是在实践中快速尝试不变性的例子,深入了解请查看查看[这个codepen](http://codepen.io/SitePoint/pen/zGYZzQ),其中包括全面实施扫雷游戏规则。

性能如何

你可能认为这会在程序中产生可怕的现象,在某些方面你会是对的。无论何时向不可变对象添加东西,我们需要通过复制现有值并添加新值来创建新实例。这肯定会比突破单个对象更加内存密集,更具计算挑战性。

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

改进变更追踪

任何UI框架中最难的任务之一是变更跟踪。这是一个普遍的挑战,EcmaScript 7提供了一个单独的API(具有更好性能)来帮助跟踪对象变化:Object.observe()。虽然很多人对这个API感到兴奋,但也有一些人则觉得这是错误的方法。在任何情况下,它不能正确地解决突变跟踪问题:

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

tiles[0].id = 2;

“tile [0]”对象的突变不会触发我们的突变观察者,因此,提出的突变跟踪机制甚至无法使用最简单的用例。不变性在这种情况下如何解决?给定应用程序状态a,并且可能是新的应用程序状态b

if (a === b) {
  // Data didn't change, abort
}

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

总结

我希望这篇文章给你提供了一些关于不变性如何帮助你改进你的代码的知识,所提供的例子可以说明这个工作的实际效果。不变性这个概念正在持续升温,这不会是你今年阅读的最后一篇文章。给它一个镜头,我保证你会很兴奋,因为我没有时间。