前言
我们在使用 JavaScript 计算 0.1+0.2 会出现不等于0.3的情况,其实不只是 JavaScript 中会存在这个问题。只要是使用 IEEE 754 规范都会存在这个问题比如说 java 中也会存在这个问题。
IEEE 754
在 IEEE 754 规范中双精度浮点数使用 64 进行存储的。
对于64为双精度浮点数来说最高位为符号位 S 紧接着11位为指数位E最后剩余的52位为有效数字 M。
计算公式:
注意以上的公式遵循科学计数法的规范,在十进制中 0<M<10,到二进制就是 0<M<2。也就是说整数部分只能是1,所以可以被舍去,只保留后面的小数部分。如 4.5 转成二进制就是 100.1,科学计数法表示是 1.001*2^2,舍去1后 M = 001。E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以约定减去一个中间数 1023,[0,1022] 表示为负,[1024,2047] 表示为正。如 4.5 的指数 E = 1025,尾数 M = 00
最终的公式变成:
有效数字
在 IEEE 754 规定在计算机内部存储有效数字 M 时,默认这个的第一位为1,此时会被舍去只保存后面部分。比如说计算机在保存 110.001 会只保存10001。这也是为什么我们在看二进制需要先找到第一个 1 出现的位置。
指数
在 IEEE 754 规定在计算机内部存储指数 E 时,在双精度浮点数中E 11 位取值范围为 [0,2047] 。为了表示指数 E 的正负所以必须减去1023。其中 [0,1022]表示负数指数,[0,1024]表示正指数。比如说 110.001 的指数为 2 ,保存时 1023+2 即 10000000001。
- 当指数位中
11位都为1时,有效数字为0时表示无穷Infinity,结合符号位就有了正无穷Infinity与负无穷之分-Infinity。
- 当指数位中
11位都为1时,有效数字不为0时表示NAN。
接下来,我们看看为什么0.1 + 0.2 !== 0.3
首先我们先来看一下十进制如何转换成二进制的,对于一个浮点数我们需要对整数与小数部分分别求值,整数部分除以2取余;小数部分乘以2取整。
我们在此举个例子,将十进制6.125转换为二进制过程如何下:
- 整数部分转换过程
1.1 6 / 2 = 3 余 0
1.2 3 / 2 = 1 余 1
1.3 1 / 2 = 0 余 1
整数部分从1.3 到 1.1 来取,最终将6转换为二进制之后结果为110
整体过程如下图所示:
- 小数部分转换过程
2.1 0.125 * 2 = 0.25 整 0
2.2 0.25 * 2 = 0.5 整 0
2.3 0.5 * 2 = 1 整 1
小数部分从2.1 到 2.3 来取,最终将0.125转换为二进制之后结果为0.001
整体过程如下图所示:
最终我们可以得到 6.125 的二进制位 110.001 即 1.10001 * 2^2 。在计算机中存储为:
0 10000000001 110001
了解完十进制转换成二进制的过程之后,我们再来看下0.1 + 0.2的计算过程
0.1 + 0.2 的计算过程
我们可以根据刚刚的规则,先看看转换0.1的值, 就能知道它转换为二进制之后的值为0.00011...(⽆限重复 0011)
再看看转换0.2的值为0.00011...(⽆限重复 0011)
分别将其转为科学计数法的结果为:
0.1 = 0.1100110011001100110011001100110011001100110011001101 * 2^-3
0.2 = 1.1001100110011001100110011001100110011001100110011010 * 2^-3
次方数不同不能直接进行计算我们需要将0.1转为-3次方,这边需要注意由于指数从-4转化为-3,此时有效位数已经超出52位由于超出部分为0直接舍去。
0.1 = 0.1100110011001100110011001100110011001100110011001101 * 2^-3
0.2 = 1.1001100110011001100110011001100110011001100110011010 * 2^-3
之后计算0.1+0.2会发生进位
10.110011001100110011001100110011001100110011001100111 * 2^-3
将-3次方转换为-2次方,这时有效数字位数大于52且被截取部分为1那么需要往前进1,转化得
1.011001100110011001100110011001100110011001100110100 * 2^-2
在计算机内存中表示
0 1111111101 1011001100110011001100110011001100110011001100110100
解决方案
方案1: 利用toPrecision(n) 和 toFixed(n)
toPrecision和toFixed它们都可以一定程度解决此类问题,但也有一些细微区别, 具体可自行查阅资料
/**
* 精确加法
*/
function add(num1, num2, precision = 12) {
return Number((num1 + num2).toPrecision(precision));
}
console.log(add(0.1, 0.2)); // 0.3
/**
* 精确加法
*/
function addByToFixed(num1, num2, n = 1) {
return Number((num1 + num2).toFixed(n));
}
console.log(addByToFixed(0.1, 0.2)); // 0.3
方案2: 小数转整数法
如果小数点为一位,则将其乘以 10(如果是两位则为 100),然后将其四舍五入,然后将四舍五入的值除以 10
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
console.log(add(0.1, 0.2)); // 0.3
方案3: 设置一个误差范围, Number.EPSILON
function numberEpsilon(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(numberEpsilon(0.1 + 0.2, 0.3)); // true
应用第三方库,如:Math.js、bigdecimal.js等
这里就不举例了,具体玩法参考其官方文档
个人建议,真实的开发业务中,如果对数据精度特别精确的,个人建议还是使用第三方库,比较它们经历有过大量的实践和测试