我们需要注意的 immutable 操作

avatar
数据可视化 @蚂蚁集团

作者简介:hustcc 蚂蚁金服·数据体验技术团队

immutable 是什么?不变的、一成不变的。在 Javascript 中一般指一个变量在经过一个 function 处理之后,可以保持入参数据不变。

一、什么是 immutable?

举个真实日常例子:前两天,在业务代码中,需要获得数据的中位数,并使用 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 的代码。分为几种情况,看起来清晰一些吧!

1. 意识不清晰

很多开发者在写代码的时候,没有从全局胜寒意识到函数必须保证 immutable,只在乎自己的函数输入输出符合要求。

开发者需要有时刻警惕的意识,一旦涉及到 object 的操作,先提醒自己注意不可变。特别是对于公共模块的开发,开源项目的开发,养成“变道就打转向灯的好习惯”。

2. 函数理解不清晰

Array 常见的函数 push、pop、shift 等,一般都能理解它们是会修改原数组数据,是 mutable 的函数。

但是对于上述例子中的 sort 函数应该就是理解上容易产生歧义(自己之前一直以为是生成一个新的数组,写完这篇,我去搜索了整个业务代码)。同样的,容易产生歧义的函数包括:

  • sort
  • reverse
  • fill
  • splice
  • delete

注意:Array 中 immutable 函数很多,以上几个函数是功能上容易误解的。

3. 使用“不靠谱”的开源库

GitHub 真是个好东西,只要你能想到的代码,基本都有参考的,特别是 JavaScript 语言的。

但是你永远不知道你使用的轮子是不是仅仅是一个课堂作业练手的。在使用一个开源模块的时候,需要观察:

  • 单测是否完善:不是指单测是不是绿色(pass),而是看单测代码是否靠谱

在完善的单测可以看到:

  1. 项目实际的使用方式,这些很可能是 README 中无法完整写出来的东西。
  2. 理解项目的模块划分,组织架构。
  3. 开发者在项目代码中扣住的开发细节点。

比如,如果 immutable 是项目的一个重要特性之一,那么在单测代码中一定会反映出来。

  • npm 下载量

就像在网上购物一样,比较懒的购物者,直接买订单数最多的爆款。

  • issues

可以关注在 issue 处理速度、issue 中搜索关键字,可以大概知道是否存在这些问题。

  • 何妨 review 一下

如果代码简单,可以简单 review 一下,抓住代码实现的关键点。

三、如何写 immutable 的代码

既然要写 immutable 的代码,那我们应该怎么做?

1. 靠谱开源项目

  1. immutability-helper:基于原始 Array 和 Object 的 immutable 操作,很好用。
  2. js-joda:日期 Date immutable 方法。
  3. immutable-js:Facebook 的 immutable 模块,定义了新的数据结构,使用成本相对高一点点。
  4. immutability-helper-x:包装 immutable-helper,使用更友好。
  5. immu:基于 Proxy 的 immutable 模块,思路新颖,但是兼容性差一点。

2. 单测约束

针对开头的 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 自动告警。

3. 解构和 spread 语法

解构是 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 语法,就可以解决我们大部分的场景。

对我们团队感兴趣的可以关注专栏,关注github或者发送简历至'tao.qit####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~

原文地址:github.com/ProtoTeam/b…