JS66 高精度运算
JavaScript中的高精度运算。
银行家舍入法
银行家舍入法是一种国际标准的进行数值取舍的标准,也叫做四舍六入五留双的规则,具体规则是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
1 | 11.556 = 11.56 ----- 六入 |
为什么要使用银行家舍入法呢?从统计学的角度讲,银行家舍入法比通常的四舍五入更精确,具体来讲:
(1)首先个位数如果不是5
,那么该怎么做就怎么做,用哪种方式取整都是一样的。
(2)个位数是5的情况,假设只有35
和45
两个数字进行取舍,如果按照四舍五入,就都向上取整了,这样就产生偏差了。
(3)而按照银行家舍入法进行“取偶”的方式,则一个向上,一个向下,就消除了偏差,就更合理了。
toFixed
方法和toPercision
方法
Number.prototype.toFixed
方法接受一个数字作为参数(如果传入的是小数,将向下取整),将数字按照指定的参数就行取舍,得到的结果字符串,如果不足位将补充'0'
在网上看到很多文章说,toFixed
方法采用的就是银行舍入法,但是下面的结果却和银行家舍入法的结果出现了出入:
1 | (2.55).toFixed(1) |
按照银行家舍入法,5
后面没有数字,前位5
为奇数应该进位,但是结果却是'2.5'
,这是为什么呢?
这是因为,银行家舍入法的前提是我们讨论的5
必须是精确的为5
,而JavaScript中使用浮点数来存储小数,实际上是有误差的:
1 | (2.55).toFixed(32); |
实际上在进行舍入的时候是对上面的数值进行舍入,所以结果自然是2.5
,从V8源码角度进行解析可以参考这篇文章:为什么(2.55).toFixed(1)等于2.5?
toPercision
方法与toFixed
方法有相同之处,但是也有不同之处:
toPercision
是处理精度,精度是从左至右第一个不为0
的数字开始toFixed
是从小数点开始数起
1 | (123.456).toFixed(1) |
toPercision
也会精度的误差,如果toFixed
一样。
浮点数误差
之所以会有误差,其实和我们都知道的JavaScript中小数运算的误差是一个原理:
1 | 0.1 + 0.2 === 0.3; // false |
因为实际上0.1 + 0.2
的结果是0.30000000000000004
。
JavaScript中使用双精度浮点数来存储数字,在将小数转换为二进制表示时的方法时“乘2取整,顺序排列”的方法,具体过程是:用2
乘以十进制小数,将积的整数部分取出,再用2
乘余下的小数部分,再将得到的积的整数部分取出,如此进行,直到积中的小数部分为零或者达到要求的精度为止。
在将0.1
转换为二进制表示时0.0001100110011001100(1100循环)
,会进行截取,截取后的二进制再转换为十进制的结果是0.100000000000000005551115123126
,这样就出现了误差。
高精度运算
上面提到了toFixed
和toPercision
都有误差,如果使用二者在进行四舍五入是很有可能差生误差的,所以不能在计算的中间过程中使用,只能用于最终结果的展示。
(1)用于展示
拿到一个数字,例如1.4000000001
时,可以使用toFixed
方法进行取整后,使用parseFloat
转为数字后进行展示,封装为函数:
1 | function strip(num, precision = 12) { |
默认精度选择12
一般能解决掉大部分0001
和0009
的问题。
(2)用于计算
如果是进行运算,就不能使用toFixed
了,需要将小数转换为整数后再进行运算,但是要注意,如果直接通过乘以10
的倍数来将小数转换为整数(或者反向的过程)同样会引入误差,比如:
1 | 20.24 * 100; |
在生产环境下,可以选择number-precision这个库来解决浮点数的误差问题,它的体积只有1K,远小于Math.js和BigDecimal.js,推荐使用,也可以学习一下它的处理方法。
大数运算
JavaScript中能够表达的最大的正整数是2^1024 - 1
,超过这个数字就变成了Infinity
:
1 | Math.pow(2, 1023) |
但是能够精确进行运算的数字范围并没有这么大,能够准确计算的最大正整数可以通过Number.MAX_SAFE_INTEGER
获得,是9007199254740991
,能够准确计算的最大负整数可以通过Number.MIN_SAFE_INTEGER
获得,是-9007199254740991
,超出这个范围的运算是不准确的。
如果要进行准确的大数运算,可以引入第三方库bigmuber.js,它把所有的数字都当做字符串,重新实现了计算逻辑,但是性能相比原生的要差很多。
在ES10的提案中提出了BigInt
数据类型,它是第七种原始类型,它是一个任意精度的数字,可以表示超过9007199254740991
的数字:
1 | const b = 1n; // 追加 n 以创建 BigInt |
如果需要进行准确的大数运算,可以根据需要支持的兼容性,来选择方案解决。