luokuning / blogs

翻译,随笔,以及懒得整理……
81 stars 2 forks source link

Immutable.js 简介 #4

Open luokuning opened 8 years ago

luokuning commented 8 years ago

一篇简单的介绍 Immutable.js 的文章,原文在这

许多开发者在处理函数式编程的时候强调数据的不可变性。函数式代码是可测试的,这是因为函数在处理数据的时候把其当成不可变的。但是在实际操作中我经常看到这个原则被打破。下面我会展示一种能从你代码中完全消除这种副作用的方法: 使用 immutable.js

救星 Immutable.js

通过 npm 安装或者直接引入源文件 immutable.min.js 就可以直接使用它。

我们的第一个例子来探索一下 immutable 的 map 数据类型。map 基本上就是一个包含键值对的对象。

var person = Immutable.Map({ 
    name: 'John', 
    birth: 594687600000,
    phone: '12345678'
});

var changePhone = function( person, newPhone ) {
    return person.set( 'phone', newPhone );
};

var person2 = changePhone( person, '87654321' );

console.log( person2 == person, person2 === person );
// false false

console.log( person.get('phone'), person2.get( 'phone' ) );
// 12345678 87654321

console.log( person.phone, person2.phone );
// underfined undefined

首先,person 拥有 name, birthphone 属性。changePhone 函数返回一个新的 immutable map。当 changePhone 调用的时候,返回值赋给了 person2 变量,这时 person2person 完全不相等。每个 map 的 phone 属性可以通过 get 方法获得。因为 map 的属性被包装在 get/set 接口中,所以不能直接获取或者修改 map 的属性值。

var person3 = changePhone( person, '12345678' );

console.log( person3 == person, person3 === person );
// true true

var person4 = changePhone( person, '87654321' );
var person5 = changePhone( person4, '12345678' );

console.log( person5 == person, person5 === person );
// false false

immutable.js 相当的智能,它能够检测一个属性是否是被设置为了跟之前一样的值 (译者注: 以 map 为例,也就是虽然调用了 set 方法,但是新的值与老值一样)。在这种情况下, ===== 都会返回 true,因为 o.set 返回的就是 o 本身。其他所有情况下,当真正的改变发生,会返回一个全新的对象引用。这就是为什么尽管 person5person 拥有完全相同的键值但是他们还是不相等。这里需要提醒你一下,在许多真实场景中,当属性值产生变动后,person 应该被丢弃,所以 personperson5 之间的比较通常没什么用。

如果我们想比较一下 personperson5 之间属性和属性值是否相等,那么可以用 map 的 equals 方法:

console.log( person5.equals( person ) );
// true

不可变数据结构虽然很棒,但是我们不是任何时候都需要它们。比如我们通常会发送 JSON 数据到服务器,而不是 immutbale.js 的数据结构。因此有需要把 immutbale.js 数据结构转换成 JavaScript 对象或者 JSON 字符串。

person5.toObject()
// Object {name: "John", birth: 594687600000, phone: "12345678"}

person5.toJSON()
Object {name: "John", birth: 594687600000, phone: "12345678"}

JSON.stringify( person5 )
// '{"name":"John","birth":594687600000,"phone":"12345678"}'

toObjecttoJSON 方法都会返回代表这个 map 数据结构的一个 JavaScript 对象。因为 toJSON 方法返回一个 JavaScript 对象,所以 immutable.js 数据结构能够直接调用 JSON.stringify 方法返回 JSON 字符串(译者注: 如果被序列化的对象有 toJSON 方法,那么 JSON.stringify 会序列化这个方法的返回值)。

如果正确使用了不可变数据结构的话,那么我们程序的可维护性自然就会得到改善。使用不可变数据结构会让你的代码没有副作用。

Immutable.js 数据结构

Immutable.js 有以下数据结构:

下面我们来简单的看一下这些数据结构。

List: List 对应于 JavaScript 中的数组。基本上所有常用的数组操作方法 List 都有,但不同的是只要改变了原始对象的内容,都会返回一个全新的 immutable 对象。

var qwerty = Immutable.List(['q','w','e','r','t','y']);

var qwerty.size
// 6

var qwertyu = qwerty.push( 'u' );
// Object {size: 7, _origin: 0, _capacity: 7, _level: 5, _root: null…}

var qwert = qwertyu.pop().pop();
// Object {size: 5, _origin: 0, _capacity: 5, _level: 5, _root: null…}

var wertArray = qwert.shift().toJSON();
// ["w", "e", "r", "t"]

var qwertyuiArray = qwert.concat( 'y', 'u', 'i' ).toJS();
// ["q", "w", "e", "r", "t", "y", "u", "i"]

Stack: 先进后出数据结构,也就是栈。对应的也是 Javascript 数组,意味着 index0 的元素将会首先被 poppedstack 中的所有元素都可以通过 get 方法获得,而不一定非得 popping 出来,但是只能通过 pushpop 方法才能修改 stack。

var twoStoreyStack = filo.push( '2nd floor', '1st floor', 'ground floor' );

twoStoreyStack.size
// 3
twoStoreyStack.get()
// "2nd floor"
twoStoreyStack.get(1)
// "1st floor"
twoStoreyStack.get(2)
// "ground floor"

var oneStoreyStack = twoStoreyStack.pop();
var oneStoreyJSON = JSON.Stringify( oneStoreyStack );
// '["1st floor","ground floor"]'

Map: 其实我们在上面的代码中已经了解过 Map 数据结构了,它对应的就是 JavaScript 对象。

OrderedMap: 可排序 map 就是混合了对象和数组的特点。你可以把它当成键根据它们被添加的顺序而被排序过的对象。修改已经存在的属性值不会改变键的顺序。

键的顺序可以通过 sortsortBy 方法被重新定义,但是注意这会返回一个全新的不可变可排序 map。

需要注意一个比较危险的地方就是可排序 map 的序列化表单值是一个简单的对象。考虑到一些语言如 PHP 把自己语言的对象当成可排序 map,理论上通过可排序 map 可以相互交互。但是为了保持清晰度,在实践中我并不推荐这种方式来进行交互。

var basket = Immutable.OrderedMap()
                      .set( 'Captain Immutable 1', 495 )
                      .set( 'The Immutable Bat Rises 1', 995 );

console.log( basket.first(), basket.last() );
// 495 995

JSON.stringify( basket );
// '{"Captain Immutable 1":495,"The Immutable Bat Rises 1":995}'

var basket2 = basket.set( 'Captain Immutable 1', 695 );

JSON.stringify( basket2 );
// '{"Captain Immutable 1":695,"The Immutable Bat Rises 1":995}'

var basket3 = basket2.sortBy( function( value, key ) { 
    return -value; 
} );

JSON.stringify( basket3 );
// '{"The Immutable Bat Rises 1":995,"Captain Immutable 1":695}'

Set: Set 就是值唯一的数组,且所有常用的数组操作方法可以用。理论上,set 中元素的顺序是无关紧要的。

var s1 = Immutable.Set( [2, 1] );
var s2 = Immutable.Set( [2, 3, 3] );
var s3 = Immutable.Set( [1, 1, 1] );

console.log( s1.count(), s2.size, s3.count() );
// 2 2 1

console.log( s1.toJS(), s2.toArray(), s3.toJSON() );
// [2, 1] [2, 3] [1]

var s1S2IntersectArray = s1.intersect( s2 ).toJSON();
// [2]

OrderedSet: 顾名思义,OrderedSet 就是根据被添加顺序排序的 Set。当你需要考虑元素的顺序的时候应该使用 OrderedSet

var s1 = Immutable.OrderedSet( [2, 1] );
var s2 = Immutable.OrderedSet( [2, 3, 3] );
var s3 = Immutable.OrderedSet( [1, 1, 1] );

var s1S2S3UnionArray = s1.union( s2, s3 ).toJSON();
// [2, 1, 3]

var s3S2S1UnionArray = s3.union( s2, s1 ).toJSON();
// [1, 2, 3]

Record: record 类似于 JavaScript 中的类,这个类拥有一些默认的键值。当实例化一个 record 的时候,定义在 record 中的键的值能够被赋值,而对于没有提供值的键则会使用 record 中的默认值。

var Canvas = Immutable.Record( { width: 1024, height: 768 } );

console.log( 'constructor ' + typeof Canvas );
// constructor function

var myCanvas = new Canvas();

myCanvas.toJSON()
// Object {width: 1024, height: 768}

myCanvas.width
// 1024

var myResizedCanvas = new Canvas( {width: 400, height: 300} );

myResizedCanvas.width
// 400

Seq: sequences 是一系列有限或者无限惰性求值的数据结构。Seq 中的元素只有在需要的时候才会去计算求值。根据类型的不同,我们可以分为 KeyedSeqIndexedSeq 或者是 SetSeq。有限或者无限的 sequences 可以这么定义:

有限的 Seqs 同样能通过计数( enumeration ) 来实现:

var oneToInfinitySeq = Immutable.Range( 1 );

var isEven = function( num ) { return num % 2 === 0; }
var evenPositiveSeq = oneToInfinitySeq.filter( isEven );

var firstTenPositivesSeq = evenPositiveSeq.take(10);
firstTenPositivesSeq.toJSON();
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

var firstTenElements = Immutable.Repeat( /* undefined */ )
                                .map( Math.random )
                                .take( 10 )
                                .toJSON();
// 生成 10 个随机数的一个数组

惰性求值的一个好处就是可以定义无限的序列,另一个好处就是性能优化。试着确定下面的代码会输出什么样的信息:

var toUpper = function( item ) {
    var upperItem = item.toUpperCase();
    console.log( item + ' has been converted to ' + upperItem );
    return upperItem;
}

var seasons = Immutable.Seq( ['spring', 'summer', 'fall', 'winter'] )
                       .map( toUpper );

console.log( 'Item at index 1: ', seasons.get( 1 ) );
console.log( 'Item at index 0: ', seasons.get( 0 ) );
console.log( 'Seasons in an array: ', seasons.toJS() );

结果可能会出人意料。考虑到求值是惰性的,并且我们处理的是一个有限的数据结构,seasons 中的元素可以直接被获取到。因此当你获取大写版本的元素时会按需求值。当 seasonstoJSON 方法被调用的时候,所有元素会被一起计算。默认的,惰性链是不会缓存计算结果的。下面是输出结果:

summer has been converted to SUMMER
Item at index 1:  SUMMER
spring has been converted to SPRING
Item at index 0:  SPRING
spring has been converted to SPRING
summer has been converted to SUMMER
fall has been converted to FALL
winter has been converted to WINTER
Seasons in an array:  ["SPRING", "SUMMER", "FALL", "WINTER"]

上面的实验对于无限序列同样适用。元素会按需求值,且没有缓存计算结果。

Summary

Immutable.js 是一个能提供不可变数据结构相当不错的库。它填充了 underscore.js 的一些缺憾: 对于不同数据结构的操作强制在 JavaScript 的数组和对象上、混合了数据类型的概念以及数据没有不可变性。尽管 lodash.js 尝试纠正其中的一些缺点,但是为了与 underscore.js 兼容还是导致了其不直观的结构。lazy.js 有懒加载功能,但是它更多的是被当成一个惰性的 underscore 版本。

immutable.js 的名字就很好的说明了在编写纯函数式代码的时候我们应该把处理不可变数据结构当成一个必要的条件。记住,使用正确的数据结构能够提高你代码的可维护性。

JesseZhao1990 commented 7 years ago

写的太棒了~~

luokuning commented 7 years ago

@JesseZhao1990 谢谢 😄