众所周知,计算机以二进制存储数字,而真实世界中用十进制表示数字。二进制和十进制的差异可以用一个乍看非常怪异的例子说明:
> 0.1 + 0.2
0.30000000000000004
不管是 JavaScript, Python 还是 Java,都能够得到类似的结果。
分析原因,就像十进制中的 1/3 是无限循环小数,二进制中的 1/10 和 1/5 也是无限循环小数。计算机的基本数字类型都是有精度限制的。两个无限循环小数相加,导致精度损失,产生了 0.30000000000000004 这样的结果。
在很多情况下,这个并不会有实际影响。因为十进制同样有精度误差,十进制并不比二进制更精确。它真正导致的问题是违反直觉。对于人直接可见的数字计算,比如购物车结算总价,出现二进制精度问题显然会让不了解二进制原理的普通用户感到诧异。
另外,原生浮点数的取整方法都是四舍五入。按照统计学规律,大量累加起来,总数会偏大。对于电商,银行和支付系统,会导致交易的某一方长期多付出,是违反公平的。因此这些系统常常采用银行家算法(四舍六入五成双)。
最后,整数和浮点数都有固定的精度限制,且受限于硬件或编译器无法更改。计算超出范围的大数会出错,不安全。
基于这三点原因,在电商和金融系统中,会使用十进制类而非基本数据类型来计算和存储交易数字。
JS 常用库比较
JavaScript 目前仍然没有原生的十进制类。我们只能从一些提供十进制类的 NPM 包中选择。不同的十进制库提供的功能不完全相同。通常功能越多体积越大。目前开发比较活跃且比较流行的 JS 库都来自同一位作者。差异详见此处。
NPM 包 | big.js | bignumber.js | decimal.js |
---|---|---|---|
打包尺寸 Minified + Gzipped | 2.9kB | 8.1kB | 12.3kB |
精度控制 | N位小数 | N位小数 | N位有效数字 |
除法精度损失 | 有损 | 有损 | 有损 |
加、减、乘法精度损失 | 无损 | 无损 | 有损 |
超大数 | 支持,计算较慢 | 支持,计算较快 | 支持,计算较快 |
超小数 | 支持,计算较慢 | 支持,计算较慢 | 支持,计算较快 |
舍入模式 | ROUND_DOWN ROUND_HALF_UP ROUND_HALF_EVEN ROUND_UP | ROUND_UP ROUND_DOWN ROUND_CEIL ROUND_FLOOR ROUND_HALF_UP ROUND_HALF_DOWN ROUND_HALF_EVEN ROUND_HALF_CEIL ROUND_HALF_FLOOR | ROUND_UP ROUND_DOWN ROUND_CEIL ROUND_FLOOR ROUND_HALF_UP ROUND_HALF_DOWN ROUND_HALF_EVEN ROUND_HALF_CEIL ROUND_HALF_FLOOR EUCLID |
指数计算 | 支持 | 支持 | 支持 |
对数计算 | 不支持 | 不支持 | 支持 |
复数计算 | 不支持 | 不支持 | 支持 |
三角函数计算 | 不支持 | 不支持 | 支持 |
从简单到复杂排列:big.js < bignumber.js < decimal.js
从打包体积来看,big.js 最小,在浏览器端占据优势。
在电商和金融场景,更常见的精度控制方法是控制 N 位小数,big.js 和 bignumber.js 更加合适。而在科学计算和工程领域,可能 decimal.js 的控制 N 位有效数字的特性会更合适一些。
除法都是有精度损失的,这是无法避免的。但是 decimal.js 的加减乘法也有精度损失,这一点需要注意。
big.js 对于超大数的计算会比 bignumber.js 或 decimal.js 慢,要结合具体场景分析是否会性能瓶颈。在前端/客户端场景,或者小型电商后端,计算量很小,且大数不常见,那么 big.js 完全可以胜任。在大型电商平台和金融交易后端,大数计算量繁重,那么 bignumber.js 会更好一些。科学和工程领域需要的超大数计算,则 decimal.js 会更合适。(但是这些领域通常也不会用 JS 这么慢的语言,所以 decimal.js 的定位有点尴尬)
超小数通常只在金融,科学和工程领域出现,这种数据的计算 decimal.js 会比 big.js 和 bignumber.js 快。和上面的大数应用场景分析类似,要视情况选择。
默认的舍入模式都是四舍五入(ROUND_HALF_UP),且都支持银行家算法——四舍六入五成双(ROUND_HALF_EVEN)。对于大多数电商和金融场景都足够用了。
decimal.js 还支持对数,复数,三角函数等高级数学运算。
按应用场景选择
中小型电商
中小型电商的交易数字都偏小,big.js 在前后端都可以胜任,满足精度控制,舍入模式和性能的需求。同时 big.js 的体积较小,有利于优化前端传输速度。
大型电商平台和金融系统
前端/客户端通常只处理单一客户的计算需求,大数出现频率较少,通常不存在性能问题。 big.js 依然可以胜任。
后端则可能会频繁处理大数,这时候 bignumber.js 会更快一些。但是,这种情况通常表明整个系统都面临性能问题,更加实际的做法是将重度计算的服务用 Java 之类的更高性能语言重构……
科学计算和工程领域
decimal.js 功能更加丰富,对极大和极小数计算的性能更好,比 big.js 和 bignumber.js 更合适。
但是,这些领域通常对性能要求很高,用 JS 的情况非常少见。建议直接使用一种更合适的语言……
总结
前端选择 big.js 一般都没错。它简单小巧,却又能提供所有我们需要的功能。
后端 Node.js 不需要考虑打包尺寸,如果有比较多的大数计算量,也可以上 bignumber.js 的。
decimal.js 的定位则比较尴尬,除非你特别需要某些科学计算功能,否则没有必要选它。
实践
- 阿里巴巴 Fusion Design 从 bignumber.js 切换到 big.js PR 链接
发表回复