现象
0.1 + 0.2 === 0.3
false
前置知识
- 在强类型语言中,整数和小数是区分对待的,整数用整型,小数用浮点型
- 目前有两种显示浮点数的方法:单精度和双精度,在进行浮点运算时,单精度使用 32 位,而双精度使用 64 位。
- 本文对浮点型简略带过,关于浮点型不了解的同学,推荐<你不知道的浮点型>
探究
以0.1 + 0.2 === 0.3
为false为例;
要解释这个问题,必须先解释js中小数存储的原理;事物出现必有其原因,问题驱动:
- js中数字类型无区分(整数、小数),那它们转化为二进制后都是怎么存储的呢?
小数有要求(乘R取整,可能出现循环无穷,可见下面的0.1),整数易转化无要求(除R取余),很简单的,要保证小数的运算标准,所以必然是浮点型存储;
- js选择的是哪种?
与许多其他编程语言不同,JavaScript 并未定义不同类型的数字数据类型,而是始终遵循国际 IEEE 754 标准,将数字存储为双精度浮点数。
IEEE754 标准的双精度浮点数
IEEE 754 浮点数由三个域组成,分别为 sign bit (符号位)、exponent bias (指数偏移值) 和 fraction (尾数)。64 位中,sign bit 占 1 位,exponent bias 占 11 位,fraction 占 52 位。
实操
好的,让我们开始计算模拟计算机开始计算0.1+0.2吧,关键是小数转IEEE 754标准,之后的加和很简单
转化过程相当于把大象装冰箱:
- 将 0.1 转换为二进制表示
- 将转换后的二进制通过科学计数法表示
- 将通过科学计数法表示的二进制转换为 IEEE 754 标准表示
将 0.1 转换为二进制表示
我们都知道小数转二进制用乘R取整的方法,运算如下(进制转换及原补反移码不了解的同学,推荐<浅显易懂的原补反移>)
得到结果0.00011001100110011…(循环0011)
将转换后的二进制通过科学计数法表示
0.00011...(无限重复 0011) 通过科学计数法表示则是 1.10011001...(重复 1001)*2-4
将通过科学计数法表示的二进制转换为 IEEE 754 标准表示
进行规格化,简言之就是求得 exponent bias 和 fraction ,
- exponent bias (指数偏移值)
双精度浮点数固定偏移值 (2^(11-1)-1) 加上指数实际值(即 2^-4 中的 -4) 的 11 位二进制表示。为什么是 11 位?因为 exponent bias 在 64 位中占 11 位
因此 0.1 的 exponent bias 等于 1023 + (-4) = 1019 的11 位二进制表示,即 011 1111 1011。
fraction(尾数)
fraction 占 52位所以抽取 52 位小数(多出来的采用四舍五入制)
1001...(中间有 11 个 1001)...1010 (请注意最后四位,是 1010 而不是 1001,因为四舍五入有进位,这个进位就是造成 0.1 + 0.2 不等于 0.3 的原因)。
到此,终于可以将 0.1 转换为 IEEE 754 表示了
0 011 1111 1011 1001...( 11 x 1001)...1010
(sign bit) (exponent bias) (fraction)
警报警报,误差出现
此时如果将这个数转换为十进制,可以发现值已经变为 0.100000000000000005551115123126 而不是 0.1 了。
同理将0.2和0.3同过程,亦会有误差。
加和自然不等。
奇怪的相等
在 javascript 语言中,Number 下分别有两个常量 MAX_VALUE 和 MAX_SAFE_INTEGER。
其中,MAX_VALUE 表示在 JavaScript 里所能表示的最大数值,MAX_SAFE_INTEGER 表示在 JavaScript 中最大的安全整数,他们的值分别如下:
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true
前置知识
Number.MAX_SAFE_INTEGER与Number.MAX_VALUE
上面的定义是不是很模糊,最大还能理解,最大安全是指什么?了解思考一番后,我们这样来理解,我们从上面已经得知了js(其实只要遵循 IEEE 754规范的都是)存储数字的规则,问题驱动:
js中最大的数怎么表示?
聪明的你肯定会想到是,从科学计数法的角度而言,自然是尾数最大,指数最大;
- 尾数即52位全取一就好了。
- 阶码即11位全取一
很简单对吗?对不起给大家挖坑了,其实不对,这个数在js中属于NAN,为什么呢?龟腚(IEEE754对三种特殊的情况进行了规定)
至于为什么有这个规定。。想了很久没想明白,战略性放弃,回头补上。
这样的话,我们就可以得到最大数了
- 尾数即52位全取一就好了
- 阶码即11位除最后一位外全取一
得到的是什么呢?(2^53 – 1) 2^(2046 – 1023) = (2^53 – 1) 2^971
这既是Number.MAX_VALUE的值
控制台打印
(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308
(Math.pow(2, 53) - 1) * Math.pow(2, 971) === Number.MAX_VALUE // true
js中安全范围内最大的整数怎么表示?
所谓的安全,就是大于这个数的整数不一定可以精确表示,其实很好理解,只有尾数溢出了52位,才会出现“四舍五入”的情况,再加上默认存在的首位1,这也就意味着,整数只要在 2^53 – 1内,都是绝对安全的,不会出现精度损失问题;
那么,为什么大于这个数,就会出现呢?我们以此数+1为例,也就是这个相等的奇怪情况
const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true
Number.MAX_SAFE_INTEGER 用原码表示是
0 10000110100 1111111111111111111111111111111111111111111111111111
IEEE 754 规格化后
11111111111111111111111111111111111111111111111111111
Number.MAX_SAFE_INTEGER+1 的原码是
100000000000000000000000000000000000000000000000000000
IEEE 754 规格化后
0 10000110101 0000000000000000000000000000000000000000000000000000 0(溢出,小于0.5r,r代表进制,舍去)
Number.MAX_SAFE_INTEGER+1 的原码是
100000000000000000000000000000000000000000000000000001
IEEE 754 规格化后
0 10000110101 0000000000000000000000000000000000000000000000000000 1(溢出,为0.5r,r代表进制,且最后一位为0,不进位舍去)
注意到我们省去掉了一位,按照向偶舍入的规则,还是不会产生进位。这个时候就有问题了,这个数跟刚才那个数竟然是相等的,我们来验证下
控制台打印
const a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true
到此,我们就真正原理上解释了js中奇怪的相等与不等现象
参考文章
JS-为什么 0.1 + 0.2 不等于 0.3 ?
浮点数阶码的计算和表示
JS 中的 MAX_VALUE 和 MAX_SAFE_INTEGER 是怎么来的
原文作者:南方小菜
原文链接:https://juejin.im/post/6870045627623931912
原文来源:掘金