JavaScript数值Number类型解析:0.1+0.2为什么不等于0.3?

1,024 阅读5分钟

从一道面试题"0.1+0.2 === 0.3",看JavaScript数值Number类型解析

分析(原来JS的Number是Double)

0.1 + 0.2 // 0.30000000000000004;
0.1+0.2 === 0.3 // false

从代码的运行结果来看,显然0.1+0.2是不等于0.3的那么导致这种结果的原因是什么呢?

JavaScript权威指南第六版-第3章类型”说明,在JavaScript当中对于数字Number类型的表示采用的是 “IEEE 754 标准
定义的双精度64位格式” 和其他编程语言(如C,Java)不同,JavaScript不区分整数值和浮点数值,所有数值在JavaScript
中均用双精度浮点数值表示(相当于C,Java中的double),所以在进行数字运算的时候要特别注意精度缺失问题。

IEEE 754标准

简单介绍“IEEE 754”规范,IEEE 754”采用双精度存储(double-precision 64-bit format IEEE 754 values)占用64 bit

image.png

意义:

  • s(sign) 1位用来表示符号位(正负数)
  • e(exponent) 11位用来表示指数
  • m(mantissa) 52位用来表示尾数

JavaScript中数字的存储机制

(s) * (m) * (2^e)

根据ECMAScript 5 规范,e 的范围是 [-1074, 971],这样可以得出 js 能表示的最大值为1 * (2^53 - 1) * (2^971) = 1.7976931348623157e+308,而这个值恰好是 Number.MAX_VALUE 的值;同理可以推出 js 能表示的大于 0 的最小值是1 * 1 * (2 ^ -1074) = 5e-324,这个值恰好是 Number.MIN_VALUE 的值。

那么浮点数在运算时又为什么会造成精度缺失呢?

0.1 >> 0.0001 1001 1001 1001...无限循环
0.2 >> 0.0011 0011 0011 0011...无限循环

由于存储空间的有限,于是只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。这即是计算机中部分浮点数运算时出现误差,丢失精度的根本原因。

大整数的精度丢失

大整数的精度丢失和浮点数本质上是一样的,尾数位最大是 52 位,因此 JS 中能精准表示的最大整数是 Math.pow(2, 53),十进制即 9007199254740992。

大于 9007199254740992 的可能会丢失精度

9007199254740992 + 1
// 丢失 9007199254740992
9007199254740992 + 2
// 未丢失 9007199254740994
9007199254740992 + 3
// 丢失 9007199254740996
9007199254740992 + 4
// 未丢失 9007199254740996

回到问题

那么为什么0.1+0.2不等于0.3呢?

  • 因为 计算机 的存储原理,造成计算机在存储浮点数时,存储的不是准确数值,存储的是一个近似数值,显示时,显示为一个浮点数值效果;
  • 当浮点数直接参与计算或者参与比较时,实际参与预算或者比较的数值,也是近似值;
  • 就造成了计算或者比较时,一定会存在误差,这个误差在特殊情况下会表现出误差的结果; ps:JavaScript中规定,即使使用科学计数法,数据类型也是浮点数类型,浮点数的误差/浮点数的精确丢失

模拟计算

先将 0.1 和 0.2 转化成二进制,对于十进制转二进制,整数部分除二取余,倒序排列,小数部分乘二取整,顺序排列

0.1 转化为二进制0.0 0011 0011 0011 0011 0011 0011 … (0011循环)

0.2 转化为二进制0.0011 0011 0011 0011 0011 0011 0011 … (0011循环)

然后根据IEEE 754标准 (s) * (m) * (2^e)来表示

// 0.1
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.2
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

//这里的m指的是小数点后的52位,e为m的指数,小数点前的整数部分就是隐藏位s来表示符号
//如果发现指数e不一致时,一般采用右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出
//转化之后进行求和

e = -3; m = 0.1100110011001100110011001100110011001100110011001101 (52位)
+
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
// 得到
e = -3; m = 10.0110011001100110011001100110011001100110011001100111 (52位)
// 保留一位整数
e = -2; m = 1.00110011001100110011001100110011001100110011001100111 (53位)
// 发现超过了52位,于是要做四舍五入,因为无法区分哪个更接近,于是规则是保留偶数的一个,得到最终的二进制数
m=1.0011001100110011001100110011001100110011001100110100 (52位)
// 然后得到最终的二进制数
1.0011001100110011001100110011001100110011001100110100 * 2^-2 = 0.010011001100110011001100110011001100110011001100110100
// 现在转化为十进制,二进制小数转化为十进制的方法是小数点后 第一位 *2 ^ -1,第二位 *2 ^ -2,以此类推
// 可以利用等比数列求和公式,最终求得十进制数为0.30000000000000004

结论

所以0.1 + 0.2 的最终结果是0.30000000000000004

补充

如何解决浮点数丢失精度的问题

其实就是根据所需把浮点数提升为整数类型就可以了,例:

    (0.1 * 10 + 0.2 * 10) / 10 === 0.3 // true