如果用非可变性来形容一个对象,对么这个对象的特点是:这个对象在创建之后不会被修改。JS中很多值是非可变的,例如:
var statement = "I am an immutable value";
var otherStr = statement.slice(8, 17);
复制
在执行完以上代码后,statement
的值并不会改变。实际上JS中所有字符串方法都不会改变原字符串,而是返回新的字符串。因为字符串是非可变的--不能被修改,只能创建新的字符串。在JS中不只有字符串是非可变,普通的数值也是非可变的。2 + 3
并不会改变2
的值。
在JS中,字符串和数值被设计为非可变的,但是很多情况下并非如此。例如下面:
var arr = [];
var v2 = arr.push(2);
复制
以上代码会使得arr
和v2
都变为[2]
。push
操作改变了原数组的内容。要想使数组满足非可变性,需要使用自定义的类似的数据结构:
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]
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}
复制
以上模拟的非可变数组和非可变Map的操作并不会改变原数据结构中的内容,而是返回新的对象。
在应用开发过程中,经常需要管理和跟踪一些状态(在很多UI框架中很常见),这个过程较困难且容易出错。使用非可变性数据结构进行开发,可以使应用中的数据流以不一样的形式来实现和管理。 在使用普通对象(可变性对象)进行开发时,当需要跟踪管理某些数据的变更,需要用到Object.observe
之类的方法来监控某个对象,并指定相应的回调函数。这种方法有两弊端:
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 2);
assert(map1 === map2); // no change
var map3 = map1.set('b', 50);
assert(map1 !== map3); // change
复制
使用非可变性对象可以将异步的“订阅者--发布者”模式变成同步的顺序逻辑,即在可能产生新数据的操作之后进行判断并处理。非可变性对象的另一个好处是克隆对象比较方便。因为非可变性对象在创建之后不会被修改,所以可以直接使用等号赋值将一个对象的引用赋给另一个对象:
var map1 = Immutable.Map({a:1, b:2, c:3});
var clone = map1;
复制
这种赋值引用的方式可以极大地节约内存。说到节约内存,非可变性对象很容易让人怀疑:“像这样有一点修改就创建一个完全的新对象,是不是会很浪费空间?”。如果在创建新对象的时候是完全开辟新的内存空间来存储原对象的所有属性,那么确实很浪费空间。但是在实现非可变性数据结构时可以采用“共享数据结构”(structural sharing)的方法,不同对象的相同值的属性可以共享,只额外保存新的属性值,和一些用于共享的引用信息,这样就可以解决内存开销过大的问题。虽然还是会有一额外的内存开销,但是相比于非可变性数据结构在其它方面带来的开发和性能方面的好处来说可以忽略。下面介绍的immutable-js也是用到了共享数据结构的方法。
immutable-js是facebook开发的JS非可变性数据结构集合。里面包含的非可变性数据结构包括List
,Stack
,Map
,OrderedMap
,Set
,OrderedSet
和Record
。这些数据结构参考了ES6中新增的一些数据结构,并有所增强。
var Immutable = require('immutable');
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 50);
map1.get('b'); // 2
map2.get('b'); // 50
复制
下面是两个主要的函数
function createGame(options) {
return Immutable.fromJS({
cols: options.cols, //列数
rows: options.rows, //行数
tiles: initTiles(options.rows, options.cols, options.mines) //单元格列表
});
}
/*初始化后的数据结构形如:
{
cols: 2,
rows: 2,
tiles: [{id: 0,isRevealed: false},{id: 1,isRevealed: false},
{id: 2,isRevealed: false},{id: 3,isRevealed: false}]
}
*/
复制
function revealTile(game, tile) {
return game.getIn(['tiles', tile]) ? //判断揭开的单元格id是否存在
game.setIn(['tiles', tile, 'isRevealed'], true) :
game;
}
复制
作者在后续还分析了ES7中的Object.observe()
方法并不能很好地解决UI框架中的状态跟踪问题。例子也是使用上面的扫雷:
var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}];
Object.observe(tiles, function () { /* ... */ });
tiles[0].id = 2;
复制
在使用原生数组存储单元格信息时,使用Object.observe()
不能捕捉到tiles
中某个元素的属性被修改。