本文由云+社区发布
相信大家在日常开发中都遇到过浮点数运算精度误差的问题,比如.log(0.1+0.2===0.3)// false。 在 中,所有数字(包括整数和小数)都由类型表示。 本文通过介绍的二进制存储标准来了解浮点精度运算的问题以及对象的属性值是如何获取的。 最后介绍了一些常用的浮点精度运算解决方案。
存储标准
它采用IEEE 754定义的64位双精度浮点类型来表示。具体的字节分配,可以先看一下维基百科引用的图片:
从上图可以看到,从高到低,64位分为3段,分别是:
指数位共 11 位,取值范围 0 ~ 2047。当指数位 e=0 或 e=2017 时,根据有效位 f 是否为 0,有不同的特殊含义。具体见下表:
对于常用的,为了方便表达指数为负数的情况,对指数值做了-1023的偏移量。 对于非零数,其二进制科学记数法的第一个有效位固定为1。这样,双精度浮点数的值为
因为,它可以用来表示接近于0的数字。它的特殊之处在于有效数字前面是0而不是1,并且指数偏移了-1022,所以值为:
对象中的几个属性值
知道如何存储之后,就清楚对象的属性如何获取它们的值了。
.:可以表示的最大数字。 显然,当e和f都最大化时,可以表示的数最大,其值为
.:能表示的最小正数,用最小表示。当e=0时,f的最后一位为1,其他位为0,则最小值为
。 :表示1与大于1的最小可表示浮点数之差。其值为
.:表示 中最大的安全整数。 能够连续、准确表示的整数就成为安全整数。 例如,2^54 不是一个安全整数,因为它表示的数字与 2^54+1 完全相同,e=1077,f=0。 Math.pow(2,54)===Math.pow(2,54)+1// true。 整数转换为二进制后,小数点后就没有数字了。 用二进制科学计数法表示时,小数点后最多可以保留52位,加上前导1,就有53位,所以当一个数转换成二进制时,如果位数超过53位,则结束部分会被截断,也就是说无法准确表达,是一个不安全的整数。 因此,将被截断的最小整数是 100...001=2^53+1(中间有 52 个零)。 如果这个数字设置为X,则小于 的整数。最终值为
.:表示 中最小的安全整数,. 取负值即可,值为-90991
为什么0.1+0.2不等于0.3
现在看 .log(0.1+0.2===0.3)// false 这题,数字0.1转成二进制是0.11...即1....1001 * 2^-4(小数部分有52位,即有13个1001循环)。 由于第53位为1,与十进制舍入类似,二进制是“零舍入”,所以最终0.1的二进制科学计数法表示为1....1010 * 2^-4,即实际的二进制值大小。上限为 0.1……。 以下代码验证该值(打印的值已删除最后一个 0):
var a = 0.1;console.log(a.toString(2)); //0.0001100110011001100110011001100110011001100110011001101
同理,十进制数0.2转换成二进制最终的值为1....1010 * 2^-3,即0....; 十进制0.3转换为二进制的最终值为1....0011 * 2^-2
var b = 0.2;console.log(b.toString(2)); //0.001100110011001100110011001100110011001100110011001101var c = 0.3;console.log(c.toString(2)); //0.010011001100110011001100110011001100110011001100110011
因此0.1+0.2的值就是上面0.1和0.2对应的二进制值相加,如下图
上图中,对于得到的和,“零舍入”保留了52位有效小数位,即最终的值:0....(第53位为1,所以前进了1),如图以下代码。 这个值与上面最终的二进制表示的0.3明显不同,这也解释了0.1+0.2不等于0.3的根本原因(实际上,这个值换算成以10为底,近似等于0。)。 注:打印长度为54,因为有52位有效十进制数字,前面是‘0.01’,长度为4,去掉最后2个0,所以最终打印长度为52+4-2 = 54。
var d = 0.1 + 0.2;console.log(d.toString(2)); //0.0100110011001100110011001100110011001100110011001101console.log(d.toString(2).length); // 54
浮点精度运算解决方案
关于js浮点运算精度损失的问题,不同的场景可以有不同的解决方案。 1、如果只是用来显示浮点数的结果,可以借用对象的and方法。 下面的代码片段中,fixed参数表示保留多少位小数,精度可以根据实际场景进行调整。
function formatNum(num, fixed = 10) { return parseFloat(a.toFixed(fixed))}var a = 0.1 + 0.2;console.log(formatNum(a)); //0.3
2、如果需要对浮点数进行加、减、乘、除等运算,从上面可以看出,小于范围内的整数。 可以准确表示,因此可以先将小数转换为整数,然后再将结果转换为相应的小数。 例如,两个浮点数相加:
function add(num1, num2) { var decimalLen1 = (num1.toString().split('.')[1] || '').length; //第一个参数的小数个数 var decimalLen2 = (num2.toString().split('.')[1] || '').length; //第二个参数的小数个数 var baseNum = Math.pow(10, Math.max(decimalLen1, decimalLen2)); return (num1 * baseNum + num2 * baseNum) / baseNum;}console.log(add(0.1 , 0.2)); //0.3
参考
本文已获作者授权腾讯云+社区发布