immutable 是什么?不变的、一成不变的。在 Javascript 中一般指一个变量在经过一个 function 处理之后,可以保持入参数据不变。
举个真实日常例子:前两天,在业务代码中,需要获得数据的中位数,并使用 echarts 绘制出中位线,辅助分析。写代码之前,先找了一下中位线的定义。
对于有限的数集,可以通过把所有观察值高低排序后找出正中间的一个作为中位数。如果观察值有偶数个,通常取最中间的两个数值的平均值作为中位数。
哦,挺简单,于是奋笔疾书(其实是 github 上的代码片段)
/**
* 求取数组中的中位数
* [1, 2, 3, 4, 4] -> 3
* [1, 2, 3, 4, 5, 6] -> 3.5
*/
const calculateMedian(arr) => {
// 1. 排序
arr.sort((a, b) => a - b);
const half = Math.floor(arr.length / 2);
// 2. 按照中位数定义计算
// 奇数长度则中间值,偶数长度则中间两个数的平均值
return arr.length % 2 !== 0 ?
arr[half] :
(arr[half - 1] + arr[half]) / 2.0;
}
这段代码的问题在于函数是 mutable 的:
当经过函数 calculateMedian 求取数组 [5, 4, 3, 2, 1] 的中位数的时候,导致入参数组变成了 [1, 2, 3, 4, 5]。导致后续的代码拿到的数组,都是经过排序的,数据都是混乱的。
上述代码只需要修改一个地方即可:
const newArr = [].concat(arr).sort((a, b) => a - b);
其中 [].concat(arr)
达到的效果就是代码的 immutable。
通过这样的一个实际例子,我想应该很容易理解 mutable 代码造成的影响是什么?
- 数据污染:经过一个方法,数据就变化了,多么可怕。
- 数据不可控:数据不仅仅是被修改,而且我们没法控制数据被修改的规律。
- debug 黑洞:一般的 bug,如果了解业务,可能不用 debugger 就能定位一二;mutable 产生的 bug,没办法,新增代码一句句 F10 吧。
很多情况下,道理我们都懂,但是还是会不经意写出 mutable 的代码。分为几种情况,看起来清晰一些吧!
很多开发者在写代码的时候,没有从全局胜寒意识到函数必须保证 immutable,只在乎自己的函数输入输出符合要求。
开发者需要有时刻警惕的意识,一旦涉及到 object 的操作,先提醒自己注意不可变。特别是对于公共模块的开发,开源项目的开发,养成“变道就打转向灯的好习惯”。
Array 常见的函数 push、pop、shift
等,一般都能理解它们是会修改原数组数据,是 mutable 的函数。
但是对于上述例子中的 sort
函数应该就是理解上容易产生歧义(自己之前一直以为是生成一个新的数组,写完这篇,我去搜索了整个业务代码)。同样的,容易产生歧义的函数包括:
- sort
- reverse
- fill
- splice
- delete
注意:Array 中 immutable 函数很多,以上几个函数是功能上容易误解的。
GitHub 真是个好东西,只要你能想到的代码,基本都有参考的,特别是 JavaScript 语言的。
但是你永远不知道你使用的轮子是不是仅仅是一个课堂作业练手的。在使用一个开源模块的时候,需要观察:
- 单测是否完善:不是指单测是不是绿色(pass),而是看单测代码是否靠谱。
在完善的单测可以看到:
- 项目实际的使用方式,这些很可能是 README 中无法完整写出来的东西。
- 理解项目的模块划分,组织架构。
- 开发者在项目代码中扣住的开发细节点。
比如,如果 immutable 是项目的一个重要特性之一,那么在单测代码中一定会反映出来。
- npm 下载量
就像在网上购物一样,比较懒的购物者,直接买订单数最多的爆款。
- issues
可以关注在 issue 处理速度、issue 中搜索关键字,可以大概知道是否存在这些问题。
- 何妨 review 一下
如果代码简单,可以简单 review 一下,抓住代码实现的关键点。
既然要写 immutable 的代码,那我们应该怎么做?
- immutability-helper:基于原始 Array 和 Object 的 immutable 操作,很好用。
- js-joda:日期 Date immutable 方法。
- immutable-js:Facebook 的 immutable 模块,定义了新的数据结构,使用成本相对高一点点。
- immutability-helper-x:包装 immutable-helper,使用更友好。
- immu:基于 Proxy 的 immutable 模块,思路新颖,但是兼容性差一点。
针对开头的 calculateMedian
函数,写一个单测:
describe('calculateMedian', () => {
test('immutable', () => {
const a = [5, 4, 3, 2, 1];
const aClone = a.slice();
expect(calculateMedian(a)).toBe(3);
// 保证不能被修改!
expect(a).toEqual(aClone);
});
});
单测约束之后,以后其他人修改这么代码导致 mutable 的时候,也会 ci 报错的,保证这个方法的长治久安。
除此之外,开启 Eslint 工具,也可以对 Object 的 mutable 自动告警。
解构是 spread 预发是 ES6 中的新语法,也是我个人用的非常多的,一般代码的 immutable 写法,都可以满足。
- Object 解构与 spread
比如我们有一个用户信息 A(几十项信息),然后需要创建一个用户信息 userB,除 id、name 不一样之外,其他都和 userA 一致。
const userA = {
id: 1,
name: 'hustcc',
addr: 'Hangzhou',
birth: '1992-08-01',
// ...
// 很多的数据不列出了
};
// 结构和 spread 写法创建 userB
const userB = {
...userA,
id: 2,
name: 'ProtoTeam',
};
- 数组 immutable 操作
const arr = [1, 2, 3, 4, 5];
// push 操作
const arrPush = [
...arr,
6,
];
// shift 操作
const [e, ...arrShift] = arr;
// concat
const arrConcat = [...arr, ...arr];
在 react + redux 技术栈下,不可变数据结构是大家都提倡的做法,所以又很多框架层面的东西帮我们处理了这些事情。
但是实际上,我们在平时的业务代码开发中,就需要有 immutable 的意识,善用 ES6 语法,就可以解决我们大部分的场景。