Javascript中的所有数值都以IEEE 754 64位双精度浮点数格式存储,这意味着在Javascript语言的底层不区分浮点数和整数,整数也是以浮点数的形式存在的。这里容易让人迷惑的是,Javascript中有些运算只有整数才能完成,此时Javascript会自动把64位的浮点数转换为32位整数再进行运算,运算完成之后再把结果转换为64位浮点数。

一、IEEE754 浮点数标准的制定背景

早期人们提出浮点数定义时,每个计算机厂商会定义自己的浮点数规则,不同厂商对同一个数表示出的浮点数是不一样的。这就会导致,一个程序在不同厂商下的计算机中做浮点数运算时,需要先转换成这个厂商规定的浮点数格式,才能再计算,这也必然加重了计算的成本。
于是,1985年,IEEE 组织推出了浮点数标准,就是我们经常听到的 IEEE754 浮点数标准,这个标准统一了浮点数的表示形式,并提供了 2 种浮点格式:

  • 单精度浮点数 float:32 位,符号位 s 占 1 bit,指数 e 占 8 bit,小数数 f 占 23 bit
  • 双精度浮点数 float:64 位,符号位 s 占 1 bit,指数 e 占 11 bit,小数 f 占 52 bit

二、JS中浮点数在内存中的存储结构

根据国际标准IEEE 754 ,Javascript浮点数的64个二进制位,从最左边开始,是这样组成的:
IEEE754-64位存储模型.jpg

  • 符号位(sign,S,浅蓝色部分):第1位用来存储符号位,表示浮点数是正数还是负数,0表示正数,1表示负数。
  • 阶码位(exponent,E,绿色部分):第2位到第12位(共11位),用来存储指数,所以又称指数位,其在内存中是以无符号整数形式存储的。
  • 尾数位(Mantissa,M,红色部分):第13位到第64位(共52位),用来存储小数(fraction)。
    符号位决定了一个数的正负,指数位决定了数值的大小(取值范围),尾数位决定了数值的精度(有效数字)。
    浮点数是采用科学计数法来表示一个数字的,它的格式可以写成这样:

    (-1)^S * M * 2^E
  • 符号部分 0 或者 1。
  • M的范围为1<=M<2,使用52位表示。 IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去。这样做的目的,是节省1位有效数字,所以加上省略的这一位数字,实际可以保存53位有效数字。
  • 阶码位长度为11bit,所以能表示的数字范围为0~2047。当阶码位全0或全1时有特殊用途:阶码位全为0,尾数全为0时,表示±0以及接近于0的很小的数字;阶码位全为1,尾数M全为0时,表示±无穷大。所以去除这两个特殊值,阶码位表示的数字范围为 [1,2046]。但是,科学计数法是可以出现负数的,所以IEEE 754规定,存入内存中E的真实值必须再加上一个中间数,对于11位的E,这个中间数是1023,所以阶码位的取值范围是[-1022, 1023]。

三、Number.MAX_VALUE 最大数

按照64位浮点数的存储结构模型,理论上说当符号位为0,阶码位全为1,尾数位全为1时,表示的数最大。但其实不然,这里IEEE754规定了另外一种情形:当阶码E全为1,尾数M不全为0时,表示非数值“NaN”。所以这种情形,得到的是 NaN。
阶码位的取值范围是[-1022,1023],所以最大数的存储结构如下:

  • 阶码位:0
  • 指数位:11111111110
  • 尾数位:1111111111111111111111111111111111111111111111111111

转换成二进制的科学计数法表示如下:

1.1111111111111111111111111111111111111111111111111111 * 2^1023
= 11111111111111111111111111111111111111111111111111111 * 2^971
= (2^53 - 1) * 2^971
= 1.7976931348623157e+308

浏览器调试窗口验证结果如下:
Number.MAX_VALUE.jpg

四、Number.MIN_VALUE 最小数

按照64位浮点数的存储结构模型,当符号位为0,阶码位全为0,尾数位最后一位为1时,表示的数最小。这里IEEE754又规定了一种情形:当阶码E全为0,尾数M不全为0时,表示非规格化小数 ±(0.xx...x) * 2 ^ -1023。有效数字M不再加上第一位隐含的1,而是还原为0.xxxxxx的小数。
最大数的存储结构如下:

  • 阶码位:0
  • 指数位:00000000000
  • 尾数位:0000000000000000000000000000000000000000000000000001

转换成二进制的科学计数法表示如下:

0.0000000000000000000000000000000000000000000000000001 * 2^(-1023)
= 2^(-51) * 2^(-1023)
= 2^(-1074)
= 5e324

浏览器调试窗口验证结果如下:
Number.MIN_VALUE.jpg

五、Number.MIN_SAFE_INTEGER 和 Number.MAX_SAFE_INTEGER 安全整数范围

安全整数中的“安全”指的是能够精确表示整数并正确比较它们。例如,Number.MIN_SAFE_INTEGER - 1 === Number.MIN_SAFE_INTEGER - 2 的结果将为真,这在数学上是不正确的。
我们从尾数 M 来分析,精度最多是 53 位(包含规格化的隐含位1),精确整数的范围其实就是 M 的最大值,即 1.11111111...111 ,也就是 2^53-1 , 使用 JS 函数 Math.pow(2,53)-1 计算得到数字 9007199254740991。

所以整数的范围其实就是 -9007199254740991 ~ 9007199254740991

我们也可以使用 JS 内部常量来获取下最大与最小安全整数

Number.MIN_SAFE_INTEGER  // -9007199254740991
Number.MAX_SAFE_INTEGER  //  9007199254740991

如果整数是这个范围内,则是安全整数。一个整数是否是安全整数可以使用 JS 的内置方法 Number.isSafeInteger() 来验证。

六、Number.EPSILON

Number.EPSILON 属性表示1和大于1的最小值的差值。
对于64位浮点数来说,大于1的最小浮点数相当于二进制的1.00…001,小数点后面有连续51个零。这个值减去1之后,就等于2的-52次方。

Number.EPSILON === Math.pow(2, -52)

Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的,因此,Number.EPSILON的实质是一个可以接受的最小误差范围。

经典问题:如何解决0.1+0.2不等于0.3?
常规解决方案:把计算数字提升10的N次方成为一个整数,两个整数进行计算就不会有误差了,如下:

(0.1 * 1000 + 0.2 * 1000) / 1000 == 0.3

使用Number.EPSILON解决方案:

if (!Number.EPSILON) {
  Number.EPSILON = Math.pow(2, -52);
}

// 判断实际值和计算值是否相等(即是否在误差范围内)
function eqaul(n1,n2){
  return Math.abs(n1-n2) < Number.EPSILON;
}


console.log(eqaul(0.1+0.2,0.3));