JS66 高精度运算

JavaScript中的高精度运算。

银行家舍入法

银行家舍入法是一种国际标准的进行数值取舍的标准,也叫做四舍六入五留双的规则,具体规则是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一

1
2
3
4
5
11.556 = 11.56  -----   六入
11.554 = 11.55 ----- 四舍
11.5551 = 11.56 ----- 五后有数进位
11.545 = 11.54 ----- 五后无数,若前位为偶数应舍去
11.555 = 11.56 ----- 五后无数,若前位为奇数应进位

为什么要使用银行家舍入法呢?从统计学的角度讲,银行家舍入法比通常的四舍五入更精确,具体来讲:

(1)首先个位数如果不是5,那么该怎么做就怎么做,用哪种方式取整都是一样的。

(2)个位数是5的情况,假设只有3545两个数字进行取舍,如果按照四舍五入,就都向上取整了,这样就产生偏差了。

(3)而按照银行家舍入法进行“取偶”的方式,则一个向上,一个向下,就消除了偏差,就更合理了。

toFixed方法和toPercision方法

Number.prototype.toFixed方法接受一个数字作为参数(如果传入的是小数,将向下取整),将数字按照指定的参数就行取舍,得到的结果字符串,如果不足位将补充'0'

在网上看到很多文章说,toFixed方法采用的就是银行舍入法,但是下面的结果却和银行家舍入法的结果出现了出入:

1
(2.55).toFixed(1)

按照银行家舍入法,5后面没有数字,前位5为奇数应该进位,但是结果却是'2.5',这是为什么呢?

这是因为,银行家舍入法的前提是我们讨论的5必须是精确的为5,而JavaScript中使用浮点数来存储小数,实际上是有误差的:

1
2
(2.55).toFixed(32);
// "2.54999999999999982236431605997495"

实际上在进行舍入的时候是对上面的数值进行舍入,所以结果自然是2.5,从V8源码角度进行解析可以参考这篇文章:为什么(2.55).toFixed(1)等于2.5?

toPercision方法与toFixed方法有相同之处,但是也有不同之处:

  • toPercision是处理精度,精度是从左至右第一个不为0的数字开始
  • toFixed是从小数点开始数起
1
2
3
4
(123.456).toFixed(1)
// "123.5"
(123.456).toPrecision(1)
// "1e+2"

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,这样就出现了误差。

高精度运算

上面提到了toFixedtoPercision都有误差,如果使用二者在进行四舍五入是很有可能差生误差的,所以不能在计算的中间过程中使用,只能用于最终结果的展示。

(1)用于展示

拿到一个数字,例如1.4000000001时,可以使用toFixed方法进行取整后,使用parseFloat转为数字后进行展示,封装为函数:

1
2
3
function strip(num, precision = 12) {
return +parseFloat(num.toFixed(precision));
}

默认精度选择12一般能解决掉大部分00010009的问题。

(2)用于计算

如果是进行运算,就不能使用toFixed了,需要将小数转换为整数后再进行运算,但是要注意,如果直接通过乘以10的倍数来将小数转换为整数(或者反向的过程)同样会引入误差,比如:

1
2
3
4
5
20.24 * 100;
// 2023.9999999999998

3 * 0.1
// 0.30000000000000004

在生产环境下,可以选择number-precision这个库来解决浮点数的误差问题,它的体积只有1K,远小于Math.js和BigDecimal.js,推荐使用,也可以学习一下它的处理方法。

大数运算

JavaScript中能够表达的最大的正整数是2^1024 - 1,超过这个数字就变成了Infinity

1
2
3
4
5
Math.pow(2, 1023)
// 8.98846567431158e+307

Math.pow(2, 1024)
// Infinity

但是能够精确进行运算的数字范围并没有这么大,能够准确计算的最大正整数可以通过Number.MAX_SAFE_INTEGER获得,是9007199254740991,能够准确计算的最大负整数可以通过Number.MIN_SAFE_INTEGER获得,是-9007199254740991,超出这个范围的运算是不准确的。

如果要进行准确的大数运算,可以引入第三方库bigmuber.js,它把所有的数字都当做字符串,重新实现了计算逻辑,但是性能相比原生的要差很多。

在ES10的提案中提出了BigInt数据类型,它是第七种原始类型,它是一个任意精度的数字,可以表示超过9007199254740991的数字:

1
2
3
const b = 1n;  // 追加 n 以创建 BigInt
typeof b;
// bigint

如果需要进行准确的大数运算,可以根据需要支持的兼容性,来选择方案解决。

参考