1位 => 1 => [0, 1]
2位 => 11 => [0, 3]
3位 => 111 => [0,7]
......
n位 => 11111111111 => [0, 2^n - 1 ]
通过这个推导也能得出一个显而易见的结论:当一个二进制数每一位都为1时,就代表他所能表示的最大数字。
在 IEEE 754中使用 exponent 的首字母E来表示指数位
。
图中的粉色部分就表示科学计数法的小数位,从第0位开始到第51位结束总共52位表示的是有效数字,在这里也叫小数位。 所以很多文章里提到粉色部分小数位来表示数值的精度,不要把它理解为就是小数点后面的一个大于0小于1的数字。这个精度指的是具体精确到哪一位而不是代表小数部分保留几位小数的精度,参考上面科学计数法中的例子。
同理这里的52位所表示的范围也不是指[0, 52], 而是[0, 2^52 - 1]。
在 IEEE 754中使用 fraction 的首字母F来表示有效数字,专业一点也叫分数位
。
这一位是科学计数法中所没有的。计算机中数字分为有符号数
和无符号数
,就是数字有无正负号区分。有符号数是用最高位也就是63位当做符号位,很显然JavaScript是有符号数。
符号位以-1为底数,位的值为指数。所以当指数为0时,-1的0次幂为1表示正数;当指数为1时,-1的一次幂为-1表示负数。即符号位等于0时表示正数,符号位等于1时表示负数
;
在 IEEE 754中使用 sign 的首字母s来表示符号位
。
根据上面对标十进制位的科学计数法介绍,我们可以将计算机存储的64位二进制数字表示为:
注意:此图中1.F的1指的是首位,指数E下方的2表示的是二进制。前面有提到这两位在计算机二进制存储时没有存储,在计算时需要手动添加。上图中为添加后的结果。
到这里我们已经将IEEE 754标准的二进制数表示为了能看得懂的十进制数字。但这还不是最终的结果,我们需要对指数范围内的值进行两次处理。
在存储中,指数部分的11位都是0(对应十进制的0)和指数部分11位都是1(对应十进制的2047)是两个特殊的数字。二者不能按照我们既定的公式来参与运算,被称为非规约数
。这里暂时不需要理解非规约数的概念,只需知道掐头去尾即可。因此指数E的范围变为[1, 2046]。
此外,IEEE 754标准还制定了一个指数偏移值
。指数偏移值的用法就是需要在原来的指数基础上减去这个值
。这个值的大小为 2^(n-1) - 1
,其中n的值为位数, 对应到11位指数就是 2^10 - 1 = 1023。因此指数E的范围在原来的基础上变为[-1022, 1023]。
至于为什么要进行指数偏移,参考百度百科的介绍:
指数偏差(表示法中的指数为实际指数减掉某个值)为 ,其中的e为存储指数的比特的长度。减掉一个值因为指数必须是有号数才能表达很大或很小的数值,但是有号数通常的表示法——补码(two's complement),将会使比较变得困难。为了解决这个问题,指数在存储之前需要做偏差修正,将它的值调整到一个无符号数的范围内以便进行比较。此外,指数采用这种方法表示的优点还在于使得浮点数的正规形式和非正规形式之间有了一个平滑的转变。
说人话就是:不偏移的话不太好表示负数。
经过上面两次对指数的修正后,IEEE 754所表示的数字计算方式如下:
Number.MAX_VALUE
这个值表示JavaScript可以表示的最大数值,通常是1.7976931348623157e+308。
一个小思考:既然这个数是JavaScript能表示的最大数值,那他是不是无穷大Infinity呢?
这个值通过公式可以非常轻松的推导出来。根据上面科学计数法的介绍,指数表示他的范围,所以值最大时指数应该最大,因此E取2046。
其次,在上面推导指数位最大值时也得出一个结论,那就是每一位都是1时所表现的值最大。因此这里F的52位都取为1。
到这里还有最后一个问题那就是1.F(52个1)是二进制的,那他转换成十进制是多少呢???
这里要用到一些数学转换,上面推导出指数位最大值的结果是2^n - 1。因此计算二进制数(53个1)要比计算1.F(52个1)方便的多,所以要将小数点后移52位。怎样将小数点后移52位呢?这里同样采用好理解的十进制来举例。
11 => 1.1 => 11 * 10^-1
111 => 1.11 => 111 * 10^-2
1111 => 1.111 => 1111 * 10^-3
......
1(n+1个1) => 1.F(n个1) => 1(n+1个1) * 10^-n
所以最后1.F(52个1)就可以使用(53个1)*2^-52来表示,而53个1的二进制转换成十进制就是 2^53 - 1。所以到最后,公式就变为:
最后在代码中验证结果
var a = (2 **53 - 1) * 2 ** 971;
var b = Number.MAX_VALUE;
console.log(a);
console.log(a === b);
Number.MIN_VALUE
会算了最大数,最小数还不简单么?指数E取最小值1,分数F取52个0。1.F(52个0)事实上就是1.000...,所以最后计算下来就是2^-1022。
var a = 2 **-1022;
var b = Number.MIN_VALUE;
console.log(b);
console.log(a === b);
哪里出了问题???5e-324是哪里来的???
这里就要了解IEEE 754中关于规约浮点数和非规约浮点数的概念了。
规约浮点数和非规约浮点数
前面在指数调整时提到要对指数进行掐头去尾,而被去掉的头尾结合分数部分的值构成了IEEE 754的分类标准。根据这个分类标准,将数值类型分为了三类。规约数、非规约数和特殊值。
如果浮点数中指数部分的编码值在0 < 指数 ≤ 2^e - 2之间,且在科学表示法的表示方式下,分数 (fraction) 部分最高有效位(即整数字)是1,那么这个浮点数将被称为规约形式的浮点数。
是不是很难理解?2^e-2又是啥?
其实上面计算的MAX_VALUE用到的就是规约数。其中指数e的范围是0到2^e-1,经过掐头去掉0,去尾去掉2^e-1,那不就剩下0 < 指数 ≤ 2^e - 2了吗。其次分数 部分最高有效位(即整数字)是1指的就是我们上面科学计数法提到的首位是1。
如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约形式来表示。 IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。
是不是很难理解?
翻译为人话就是首位是0,分数部分不为0,这个时候他的指数偏移值比规约数的1023小1也就是1022。
科学计数法提到二进制的首位可以取0或1,但之前都是以规约数(首位是1)的情况在计算。因为当首位是0时,无论指数位是多少计算出的值都是一个大于0小于1的数。
例如(0.111...)2转换为十进制一定是个大于0小于1的数,无论给这个数多少次幂,它的结果会无限接近与0。比如0.5的100次幂。所以当我们要表示一个大于0小于1的值时要采用非规约数的算法。
共有三个特殊值需要记忆。
指数E为0,分数F也是0。很好理解那不就是0.000 * 2^0,那不就是0嘛,加前面符号位就是±0
;
指数e为最大值2^n-1,分数f为0,相当于 1.000... * 2^1024,所以表示无穷大正无穷
。
这里有一个非常难理解的疑问,我思考了很久。分数为0时整个分数位就是1.0000...相对比较小,为什么正无穷不是分数都是1在乘以指数最大?比如说1.1(52个1)乘以1024(11个1),也就是64位全部为1的情况下?
说明这个问题需要通过一个简单的例子:
1111 => 15 => 1.111*2^3
=> 16 => 1.000*2^4
11111 => 17 => 1.0001*2^4
注意:第一行十进制表示15时,在内存中只占了4位,此时他所能表达的最大的数字就是16。
即分数都为0,指数向上升一级的情况。如果他想在继续增大则可以在分数位上添1。
但是这个时候在内存中已经变成了5位存储而不是原来的4位存储。
在IEEE 754中,指数位E的大小由一个11位的数控制,因此他的最大指数是2047,减去偏移量1023后变为1024。此时指数位的1024所对应的11位就相当于上面15时所对应的4位。因此在表示指数的位数不晋级的情况下,所能表示的最大数字便是2^1024。
指数E为最大值2^n-1,分数不全为0,表示NAN
。
理解上面的疑问后这里就很好理解了,当分数不全为0时已经超过了可以最大表示的数,超过了内存中最大的11位。
最小值计算
最小值是一个无限接近于0的数,根据上面的介绍,应该采用非规约数的计算方式而不是规约数。即首位为0而不是1,这也是为什么通过规约数得不到最小值的原因。
非规约数中不用掐头去尾,反而要将规约数的部分去掉(1 ~ 2046)。去掉后只剩余0和2047,因此取最小值0。再减去偏移值1022(非规约数少1),所以E的最后值为-1022。
因为非规约数中首位为0,所以分数部分不能为最小值0,否则就是0.0000乘以任何数都得0了。因此要比0大一点点,所以F为000000……1(52位)。
最后的结果为:
var a = 2 **-1074;
var b = Number.MIN_VALUE;
console.log(a);
console.log(a === b);
Number.POSITIVE_INFINITY
前面特殊值中已经介绍过了,IEEE 754 所能表示的最大值并不是64个1,而是2^1024。即第0 ~ 51位分数位全为0,第52 ~ 62指数位全为1,63位符号位为0表示正数。
var a = 2 **1024;
var b = Number.POSITIVE_INFINITY;
console.log(a);
console.log(a === b);
这里也回答了上面Number.MAX_VALUE提出的问题,Number.MAX_VALUE
表示的是规约数的最大值
,却不是JavaScript所能表示的最大值Infinity
。
Number.NEGATIVE_INFINITY
var a = -(2 **1024);
var b = Number.NEGATIVE_INFINITY;
console.log(a);
console.log(a === b);
Number.MAX_SAFE_INTEGER
这个值表示的是JavaScript所能表示的最大整数,很多文章一上来就告诉你是2^53 - 1
,并没有解释清楚原因。
有的作者说分数位全是1已经无法继续添加1,因此是52。也有的说分数位填满后只能在指数位增加,指数位增加后变为原来的2倍了。然而我们不就是求的最大整数?变为2倍以后依然在MAX_VALUE内,为何不行呢???
首先还是用好理解的十进制来举例:
假设有一个十进制数字 32345,他用科学计数法表示为3.2345*10^4,他的分数位F为2345。这时如果我们说整数只能精确到234,这代表的是什么意思呢???
意思就是在分数位上 234 === 2345 === 235 === 2346,也就是说比这一位大的数字都无法保证他就是这个数字,可能会得到意料之外的结果。 将这个值乘以指数位转换为十进制后结果就被放大的更多。32340 === 32345 === 32350
因此当我们计算最大整数位时,要以分数位的最大位数来判断。 当分位数填满时,可以通过增大指数位来获得更大的整数。但是就像上面所举的例子,无论可以获取到多大的整数,在超出规定范围后这个数就不唯一了,不安全了。
再加上被默认舍弃掉的首位1总共就是53位分数位,因此当每一位上的值都为1时,就是他所能表示的最大整数。即2^53 - 1
。
var a = 2 ** 53 - 1;
console.log(a === Number.MAX_SAFE_INTEGER);
Number.EPSILON
Number.EPSILON 属性表示 1 与 Number可表示的大于 1 的最小的浮点数之间的差值。
因此很好理解那就类似于1.00......001,所以此时指数位计算结果为0,也就是E- 1023 = 0,所以E等于1023。至于分数位类似上面的0.01,前面都是0最后面的52位是1。
经过处理后就是2^-52。
var a = 2 ** -52;
console.log(a === Number.EPSILON);
0.1 + 0.2 != 0.3
这是一个老生常谈的问题,理解了JavaScript中数据的存储方式,再看这个问题会容易很多。
产生这个问题的原因在于计算时会发生转换和移位两次精度损失。
为什么2^-4,参考十进制0.00011 = 1.1*10^-4
在这里产生了第一个精度误差。因为分数位只能取52位(加上默认的首位1才53位),后面的都会被省略掉。
科学计数法做加法计算时要先保证指数相等,指数不相等需要移位,这造成了第二次精度损失。
为什么移位? 试想一下 1.2 * 10^-2 + 1.34* 10^-3 怎么计算?指数位小的向大的看齐,将1.34 * 10^-3转换为0.134 * 10^-2。然后对位相加得1.334 * 10^-2
上面的例子可以非常清楚的看到,在移位前1.34分数位只有2位,移位后变为0.134分数位变为3位。同理0.1指数位是-4,0.2指数位是-3,所以0.1指数位要换成-3再计算。换算时表示0.1分数位的52位要整体向后移动一位。
经过第一轮转换后的精度经过这次移位精度再次损失,即使后面按照规则相加也得不到想要的结果了。
参考文档:
探秘 JavaScript 世界的神秘数字 1.7976931348623157e+308
JavaScript 浮点数之迷:0.1 + 0.2 为什么不等于 0.3?
Js数字型范围,Js能表示的最大和最小值
IEEE 754
用了一天时间,我终于彻底搞懂了 0.1+0.2 是否等于 0.3!
十进制双精度浮点数与二进制转换的网站