小小的脑袋充满大大的疑惑
关于0.1 + 0.2 === 0.3
,大家应该都知道这个结果是false。
而0.2 + 0.3 === 0.5
得到的又是true,这到底是咋肥事?
到底哪些计算是成立的?哪些又是不成立的?为什么?如何解决?
带着这些问题开始我们的探究
// 成立的 0.1 + 0.3 === 0.4 // true 0.2 + 0.2 === 0.4 // true // 不成立的 0.1 * 3 === 0.3 // false 0.2 * 6 === 1.2 // false
什么是IEEE 754?
首先我们要知道,无论是整数还是浮点数,在cpu中都是通过转为二进制计算的,整数转为二进制还是很简单的。那么,如何将浮点数转为二进制数?此时我们就要引入一个关键字:IEEE754(二进制浮点数算术标准)
关于什么是IEEE 754,维基百科中的是这么解释的:
IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。
大部分编程语言都提供了IEEE浮点数格式与算术,但有些将其列为非必需的。
有此可知,不仅是JavaScript,只要是使用了IEEE 754浮点数格式,来存储浮点类型(float 32,double 64)的任何编程语言都有这个问题!下面是我用C++举的例子:
知道了前因,下面我来看一下到底如何把浮点数转为二进制!!!
浮点数转为二进制
首先,JavaScript中的Number是采用64位存储的,这64位由3部分组成,(S:符号位,Exponent:指数域,Fraction:尾数域)。
如下图(从右到左):
综合科学计数法,可以通过以下的计算公式表示:
(-1)s * 1.F * 2E-1023
其中S表示符号,0表示正数、1表示负数
1023为偏移量,32位单精度偏移量位127,下图中的S、P、M只是叫法不同,本文以维基百科中单词(S、F、E)为准。
那么S、F、E如何获得呢?
结算过程
拿263.3举例:
1.首先先获取263的二进制,如下图
可得出263的二进制为:100000111
2.再计算0.3的二进制,如下图
根据规律我们可以看出1001是循环部分,所以得出0.3的二进制为:01001100110011001............
所以263.3的二进制表示为100000111.01001100110011001............
3. 计算S
由于263.3是正数,所有用0表示,故目前64位展示位
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
0 | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ |
4. 计算E
由于要计算为1.F * 2n的形式,所以要把100000111.01001100110011001............ 小数点移到前面,成为1.0000011101001100110011001............
小数点移动了8位,即28
由公式E-1023=8,得出E为1031,我们将1031(1024+7)转为二进制为:
10000000111
目前结果:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ | _ |
4. 计算F
目前计算的值为:1.0000011101001100110011001............
将最高位去掉,剩下的填满52位即可
即F为:0000011101001100110011001100110011001100110011001100
因第53位为1,二进制中的四舍五入(零舍一入),故最终结果如下:
0000011101001100110011001100110011001100110011001101
5.最终结果
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
大家也可通过通过这个网站直接计算出SEM的值来做验证。
0.1 + 0.2 !== 0.3
回到问题本身,我们可以通过计算0.1、0,2的二进制,经过相加计算就可以知道为什么等式不成立了。
0.1的二进制
通过在 https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html 中计算得出
2-4*1 .1001100110011001100110011001100110011001100110011010
0.2的二进制
2-3*1 .1001100110011001100110011001100110011001100110011010
计算0.1 + 0.2的二进制
得到值之后该怎么处理呢?浮点数的加法是怎么进行的?那就是先对齐、再计算。
对齐以高数位为准,我们此处的例子中-3为高位。因此需要将0.1的二进制处理下,向左移动一位。
移动前:
2-4*1 .1001100110011001100110011001100110011001100110011010
移动后:
2-3*0 .1100110011001100110011001100110011001100110011001101
然后我们计算两个二进制数:
0 .1100110011001100110011001100110011001100110011001101 +
1 .1001100110011001100110011001100110011001100110011010 =
10.0110011001100110011001100110011001100110011001100111
因为我们计算的数的格式为1.xxx,所以小数点要前移一位(此时指数由-3变成了-2),但这样尾数就53位了,根据尾数后一位1进0舍的规则,
移动前:
10.0110011001100110011001100110011001100110011001100111
移动后:
1.0011001100110011001100110011001100110011001100110100
将指数去除,最后的小数位值为:
0100110011001100110011001100110011001100110011001101
根据二进制计算规则,小数点后第一位为0.5,第二位为0.25,第三位为0.125....
由于位数太多,我们写个简单的计算函数帮助我们计算:
// 二进制小数 var binaryDecimal = '0100110011001100110011001100110011001100110011001101'; // 二进制小数点后对位值 var counterpointArr = []; // 小数点后二进制的第一个对位值 var counterpoint = 0.5; for (var i = 0; i < 52; i++) { counterpointArr.push(counterpoint); counterpoint = counterpoint / 2; } console.log(counterpointArr) // [0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078...] let sum = 0; for (var j = 0; j < binaryDecimal.length; j++) { if (binaryDecimal.charAt(j) === '1') { sum = sum + counterpointArr[j] } } console.log(sum) // 0.30000000000000004
相关知识点
特殊场景
如何解决
简单的普通场景
思路1
通过差值跟最小精度比较。
Number.EPSILON是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
function epsEqu(x, y) { return Math.abs(x - y) < Number.EPSILON; // [ˈepsɪlɒn] } console.log(epsEqu(0.1+0.2, 0.3)); // true console.log(epsEqu(268.34+0.85, 269.19)); // true
思路2
思路就是将小数转为整数,然后再转为小数表示。
//注意要传入两个小数的字符串表示,不然在小数转成二进制浮点数的过程中精度就已经损失了 function numAdd(num1/*:String*/, num2/*:String*/) { var baseNum, baseNum1, baseNum2; try { //取得第一个操作数小数点后有几位数字,注意这里的num1是字符串形式的 baseNum1 = num1.split(".")[1].length; } catch (e) { //没有小数点就设为0 baseNum1 = 0; } try { //取得第二个操作数小数点后有几位数字 baseNum2 = num2.split(".")[1].length; } catch (e) { baseNum2 = 0; } //计算需要 乘上多少数量级 才能把小数转化为整数 baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); //把两个操作数先乘上计算所得数量级转化为整数再计算,结果再除以这个数量级转回小数 return (num1 * baseNum + num2 * baseNum) / baseNum; }; console.log(numAdd('0.1', '0.2')) // 0.3 console.log(numAdd('268.34', '0.85')) // 269.18999999999994 // 其实还是因为num1 * baseNum 导致的,baseNum为浮点型,先从十进制转为二进制,二进制时丢失了精度 // 解决方法是 return修改为return (num1 * baseNum + num2 * baseNum).toFixed(0) / baseNum; // 因为我们已经确保了只需要整数位
上面的代码还是比较简单的,但针对大数、特殊场景都没做处理,我们可以使用下面的库
常见库
1.Math.js
专门为 JavaScript 和 Node.js 提供的一个广泛的数学库。支持数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。
官网:mathjs.org/
GitHub:github.com/josdejong/m…
2.big.js
GitHub:github.com/MikeMcl/big…
扔给后端【推荐】
嘿嘿,后端用的也不是float、double,而是用的bigdecimal。
ps.目前组内针对数值计算一律交给后端的。
感谢那些对我有帮助的文档:
https://juejin.cn/post/6844903680362151950
https://babbage.cs.qc.cuny.edu/IEEE-754.old/Decimal.html
https://www.bilibili.com/video/BV1i54y1y7Fn?from=search&seid=16329325986965638794