JS精度缺失问题

案例

小明是一位前端新人,经过大学期间的苦学勤练终于出山,就职某公司负责某电商平台。经过一年多的项目实战,小明码代码渐臻佳境,发现JS也:


haixing.jpg

然而有一天,测试小力找到小明,告诉小明“你写的秒杀活动价格有bug”。小明心里咯噔一下:我写的代码怎么会有bug,是你测试姿势有问题吧?想归想,嘴里还是应下来先看一下。根据小力提供的测试案例,小明很快就重现了问题:价格为1.855的秒杀商品,在前端toFixed后竟然变成了1.85。经过一番搜索,小明把bug给解决了,也知道这是JS的精度缺失导致的问题。那么,问题来了:是什么导致的精度缺失?怎么解决精度缺失问题?

  • 常见的JS精度缺失场景:
1.855.toFixed(2) // 1.85
0.1 + 0.2  // 0.30000000000000004
9999999999999999 == 10000000000000001 // true
什么导致的精度缺失

在JavaScript中,Number类型是采用IEEE754二进制浮点数算术标准双精度64位浮点数来存储的,运算也是基于二进制来运算,最终才转换成十进制的结果。由于位数限制(64bit),一些不能有穷表示的二进制数只能是真正值的近似,经过运算后偏差叠加,会出现一些意料之外的结果,也就是我们所说的精度缺失。

  • 64bit组成:
sign exponent fraction
符号位 指数域 尾数域
第1位 2~12位 13~64位
转换二进制

以上面0.1与0.2为例,在十进制里面可以有穷表示,但是转换成二进制后,它们就是无穷的。具体转换方式可以遵循“正整数除二取余,小数乘二取整”的方式:

// 0.1转二进制
0.1*2=0.2****************取出整数部分0
0.2*2=0.4****************取出整数部分0
0.4*2=0.8****************取出整数部分0
0.8*2=1.6****************取出整数部分1
0.6*2=1.2****************取出整数部分1
0.2*2=0.4****************取出整数部分0
0.4*2=0.8****************取出整数部分0
0.8*2=1.6****************取出整数部分1
0.6*2=1.2****************取出整数部分1
.
.
.
// 结果 0.1D = 00011001100110011...B
// 科学计数法表示: 2^-4*1.100110011...B
//  尾数域52 位,超过部分进一舍零:2^-4*1.10011(0011 repeat 12 times)010B
最后结果:2^-4*1.1001100110011001100110011001100110011001100110011010B

同理可以得出0.2的二进制:

0.2D = 2^-3*1.1001100110011001100110011001100110011001100110011010B

0.1 + 0.2:

  2^-4*1.1001100110011001100110011001100110011001100110011010B
+ 2^-3*1.1001100110011001100110011001100110011001100110011010B
相加时指数不一致,需要对齐:
  0.1100110011001100110011001100110011001100110011001101B
+ 1.1001100110011001100110011001100110011001100110011010B
--------------------------------------------------------------
=10.0110011001100110011001100110011001100110011001100111B

得到的二进制结果为:10.0110011001100110011001100110011001100110011001100111B
重新用科学计数法表示:2^-2*1.0011001100110011001100110011001100110011001100110100
最终转化为十进制的数值:

let b = '010011001100110011001100110011001100110011001100110100';
let num = 0;
for (let i = 0, len = b.length; i < len; i++) {
    num += b[i] * Math.pow(2, -i-1);
}
num; // 0.30000000000000004
精度缺失解决

这里介绍一个第三方库decimal.js,项目里面可以进行二次封装使用。

彩蛋

ES6 在Number对象上新增了一个极小的常量Number.EPSILON,表示JavaScript的最小精度,误差如果小于这个值,就可以把二者看成是相等的。例如:

0.1 + 0.2 === 0.3 // false
0.1 + 0.2 - 0.3 < Number.EPSILON // true

然而这个也不是无往而不利,1.1 + 1.3 - 2.4 < Number.EPSILON返回的结果再次出乎意料,是false。看到这里,小明默默把简历里的“精通JS”改成了“了解”。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。