javascript浮点精度问题

242 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第N天,点击查看活动详情

遇到的问题

某天老板让去帮忙解决同事的一个小问题,问题很简单,就是把后端返回的米转换成厘米展示在前端,比如后端返回的是5.1,那么前端应该展示成510。问题相当简单,但前端展示的却不是510,而是509.99999999999994。在控制台去验证也是得到如下结果: !

image.png 除了乘法计算有问题,是否其他的javascript计算还有问题?随便试了几个,发现也有如下的问题。

  • 加法

image.png

  • 减法

image.png

除法 image.png 产生问题的原因是什么呢?

产生的原因

在javascript里面,整数和小数都是采用64位二进制进行表示。表示的方法如下图:

image.png

1位符号位,11位为指数位,52位为尾数位,所代表的10进制数字的计算公式如下:

image.png

两个浮点数乘法的计算规则如下:

  • 符号位异或处理,两个浮点数的符号位不同则结果为1,相同则为0;
  • 指数位相加处理,得到的指数位2^(1023-e1+1023-e2);
  • 尾数位相乘处理,尾数位乘法的实际结果就是二进制的乘法,假设浮点数1的尾数位为fa,浮点数2的尾数位为fb,最后得到的尾数为: fa如下图表示: fa51fa50...fa0 fb如下图表示: fb51fb50...fb0 那么得到的尾数结果为fafb512^51+fafb502^50+...+fafb02^0。 这样计算得到的尾数应该是有104位。已经是有溢出的风险,那么就需要对尾数相乘得到的结果进行截断,截断的规则如下: 如果[第51位....第0位]的结果>2^51,则进位,尾数为[第103位...第52位]+1; 如果[第51位....第0位]的结果<2^51,则舍弃, 尾数为[第103位...第52位]; 如果[第51位....第0位]的结果=2^51,则有2种处理方式,如果[第103位...第52位]为偶数,则直接舍弃[第51位....第0位];如果[第103位...第52位]为奇数,则尾数为[第103位...第52位]+1;

学习了浮点数的原理,我们来看看5.1*100的最终结果为什么不为510。 5.1的二进制表示法为: image.png 100的二进制表示法为:

image.png

按照上面的浮点数乘法规则,最后得到的二进制结果为: 1111110111111111111111111111111111111111111111111111

转化成10进制的结果为: 509.99999999999994

二进制乘法可以参考工具: 二进制乘法

如何解决

方案

浮点数问题是一个普遍且通用的问题,相信业界一定有方案,通过调研,发现number-precision比较适合来处理,选择它的原因如下:

  • 1、有不少用户基础,周下载量在1万以上;
  • 2、最近更新时间在2个月前,说明原创作者还在维护此项目,遇到问题就可以好解决;
  • 3、使用方法比较简便; PS: 不足之处在于方法名称不怎么通用,不好记,可能也是由于作者想要尽可能地缩短方法名。

number-precision的使用方法如下:

import NP from 'number-precision'
NP.strip(0.09999999999999998); // = 0.1
NP.plus(0.1, 0.2);             // 加法,= 0.3, not 0.30000000000000004
NP.plus(2.3, 2.4);             // 加法,= 4.7, not 4.699999999999999
NP.minus(1.0, 0.9);            // 减法,= 0.1, not 0.09999999999999998
NP.times(3, 0.3);              // 乘法,= 0.9, not 0.8999999999999999
NP.times(0.362, 100);          // 乘法,= 36.2, not 36.199999999999996
NP.divide(1.21, 1.1);          // 除法,= 1.1, not 1.0999999999999999
NP.round(0.105, 2);            // 取近似数,= 0.11, not 0.1

更多用法请参考官网地址