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"]
一篇简单的介绍 Immutable.js 的文章,原文在这。
许多开发者在处理函数式编程的时候强调数据的不可变性。函数式代码是可测试的,这是因为函数在处理数据的时候把其当成不可变的。但是在实际操作中我经常看到这个原则被打破。下面我会展示一种能从你代码中完全消除这种副作用的方法: 使用 immutable.js。
救星 Immutable.js
通过 npm 安装或者直接引入源文件 immutable.min.js 就可以直接使用它。
我们的第一个例子来探索一下 immutable 的 map 数据类型。map 基本上就是一个包含键值对的对象。
首先,
person
拥有name
,birth
和phone
属性。changePhone
函数返回一个新的 immutable map。当changePhone
调用的时候,返回值赋给了person2
变量,这时person2
和person
完全不相等。每个 map 的phone
属性可以通过get
方法获得。因为 map 的属性被包装在get
/set
接口中,所以不能直接获取或者修改 map 的属性值。immutable.js 相当的智能,它能够检测一个属性是否是被设置为了跟之前一样的值 (译者注: 以 map 为例,也就是虽然调用了
set
方法,但是新的值与老值一样)。在这种情况下,==
和===
都会返回true
,因为o.set
返回的就是o
本身。其他所有情况下,当真正的改变发生,会返回一个全新的对象引用。这就是为什么尽管person5
和person
拥有完全相同的键值但是他们还是不相等。这里需要提醒你一下,在许多真实场景中,当属性值产生变动后,person
应该被丢弃,所以person
和person5
之间的比较通常没什么用。如果我们想比较一下
person
和person5
之间属性和属性值是否相等,那么可以用 map 的equals
方法:不可变数据结构虽然很棒,但是我们不是任何时候都需要它们。比如我们通常会发送
JSON
数据到服务器,而不是 immutbale.js 的数据结构。因此有需要把 immutbale.js 数据结构转换成JavaScript
对象或者JSON
字符串。toObject
和toJSON
方法都会返回代表这个 map 数据结构的一个 JavaScript 对象。因为toJSON
方法返回一个 JavaScript 对象,所以 immutable.js 数据结构能够直接调用JSON.stringify
方法返回JSON
字符串(译者注: 如果被序列化的对象有toJSON
方法,那么JSON.stringify
会序列化这个方法的返回值)。如果正确使用了不可变数据结构的话,那么我们程序的可维护性自然就会得到改善。使用不可变数据结构会让你的代码没有副作用。
Immutable.js 数据结构
Immutable.js 有以下数据结构:
下面我们来简单的看一下这些数据结构。
List:
List
对应于 JavaScript 中的数组。基本上所有常用的数组操作方法List
都有,但不同的是只要改变了原始对象的内容,都会返回一个全新的 immutable 对象。Stack: 先进后出数据结构,也就是栈。对应的也是 Javascript 数组,意味着
index
为0
的元素将会首先被popped
。stack
中的所有元素都可以通过get
方法获得,而不一定非得popping
出来,但是只能通过push
和pop
方法才能修改 stack。Map: 其实我们在上面的代码中已经了解过
Map
数据结构了,它对应的就是 JavaScript 对象。OrderedMap: 可排序 map 就是混合了对象和数组的特点。你可以把它当成键根据它们被添加的顺序而被排序过的对象。修改已经存在的属性值不会改变键的顺序。
键的顺序可以通过
sort
和sortBy
方法被重新定义,但是注意这会返回一个全新的不可变可排序 map。需要注意一个比较危险的地方就是可排序 map 的序列化表单值是一个简单的对象。考虑到一些语言如 PHP 把自己语言的对象当成可排序 map,理论上通过可排序 map 可以相互交互。但是为了保持清晰度,在实践中我并不推荐这种方式来进行交互。
Set: Set 就是值唯一的数组,且所有常用的数组操作方法可以用。理论上,
set
中元素的顺序是无关紧要的。OrderedSet: 顾名思义,
OrderedSet
就是根据被添加顺序排序的Set
。当你需要考虑元素的顺序的时候应该使用OrderedSet
。Record:
record
类似于 JavaScript 中的类,这个类拥有一些默认的键值。当实例化一个record
的时候,定义在record
中的键的值能够被赋值,而对于没有提供值的键则会使用record
中的默认值。Seq:
sequences
是一系列有限或者无限惰性求值的数据结构。Seq
中的元素只有在需要的时候才会去计算求值。根据类型的不同,我们可以分为KeyedSeq
、IndexedSeq
或者是SetSeq
。有限或者无限的sequences
可以这么定义:Immutable.Range()
,Immutbale.Repeat()
,Seqs
的改变可以通过一些函数式工具如map
,filter
.有限的
Seqs
同样能通过计数( enumeration ) 来实现:惰性求值的一个好处就是可以定义无限的序列,另一个好处就是性能优化。试着确定下面的代码会输出什么样的信息:
结果可能会出人意料。考虑到求值是惰性的,并且我们处理的是一个有限的数据结构,
seasons
中的元素可以直接被获取到。因此当你获取大写版本的元素时会按需求值。当seasons
的toJSON
方法被调用的时候,所有元素会被一起计算。默认的,惰性链是不会缓存计算结果的。下面是输出结果:上面的实验对于无限序列同样适用。元素会按需求值,且没有缓存计算结果。
Summary
Immutable.js 是一个能提供不可变数据结构相当不错的库。它填充了 underscore.js 的一些缺憾: 对于不同数据结构的操作强制在 JavaScript 的数组和对象上、混合了数据类型的概念以及数据没有不可变性。尽管 lodash.js 尝试纠正其中的一些缺点,但是为了与 underscore.js 兼容还是导致了其不直观的结构。lazy.js 有懒加载功能,但是它更多的是被当成一个惰性的 underscore 版本。
immutable.js 的名字就很好的说明了在编写纯函数式代码的时候我们应该把处理不可变数据结构当成一个必要的条件。记住,使用正确的数据结构能够提高你代码的可维护性。