相关文章推荐
正直的洋葱  ·  AIGC面试考察题1 - 知乎·  6 月前    · 
发呆的洋葱  ·  apache nifi - Using ...·  1 年前    · 
寂寞的警车  ·  SAP HR 获取 ...·  1 年前    · 

最近在看红宝书,对书中所说Number类型的最大值 MAX_VALUE 以及最小值 MIN_VALUE 很感兴趣。以前也看过类似 0.1 + 0.2 的文章并整理过笔记,但回头看时发现好多知识当时并没有深入理解。所以在查阅了大量资料,花了一天多的时间整理出这篇文章。

希望巩固知识的同时可以帮助到别人。

计算机基础知识

位bit与字节Byte

世界上有01人,一种是懂二进制的,一种是不懂二进制的。

众所周知计算机是通过二进制来存储数据的,其中的0和1就是二进制的两个数字。

在计算机中可以操作的最小内存单元被称之为 位(bit) ,一个位可以放一个0或者一个1。而我们通常在提到的电子产品容量所用到的KB、M、GB等,他的基础单元就是位。其中每8个位被称作1 字节(Byte) ,也就是最小的B,他们的相互关系由小到大如下。

  • 1B = 8bit
  • 1KB = 1024B
  • 1M = 1024KB
  • 1GB = 1024M
  • 1T = 1024GB
  • JavaScript中的Number类型遵守的是 IEEE 754 64 标准,而这个标准中的64表示的就是64位, 也就是说 遵守这种标准的数字是通过64个0或1来存储 的。

    十进制转换二进制

  • 十进制整数:不断的除以2自下而上取余数,直到除尽
  • 十进制小数:不断的乘以2自上而下取整数位,直到小数部分为0
  • 科学计数法

    科学计数法常用来表示非常大或非常小的数,相比于普通的数字表示方法更加节省空间,具体可查看百度 科学计数法

    在JavaScript中所要表示的数字范围非常大,因此 IEEE 754 64是通过 二进制科学计数法 的形式来存储数据的,而不是普通的二进制存储 。这一点非常重要,在我以前的文章中就是没有深入理解这一点导致看起来很费劲。

    科学计数法上学时都学过,当时学的是十进制。十进制相对来说很好理解,但是计算机中都是用二进制来存储,如果对于科学计数法掌握不扎实,那么很难看懂二进制的科学计数法。

    科学计数法是如何表示数字的?首先看个例子:

    普通计数法表示32345:  32345
    科学计数法表示32345:  3.2345 * 10^4 
    

    10表示科学计数法的进制

    十进制中的每一个数字为0-9,大于等于10则向前进一位。例如:9小于10则表示为单个数字9,10等于10不能用个位数来表示,要向前进一。原来的个位归为0,十位上进一。同理,二进制中每一个数字为0 ~ 1,大于等于2则向前进一位。 例如:1表示1、10表示2、11表示3、100表示4。

    4被称为指数,用来表示这个数的范围。

    这个值可以是正数也可以是负数。当数字超过当前指数下的最大值时,指数需要进一。例如3.2345变为9.9999,在大一时,就需要指数进一变为5,变为1.01051.0*10^5

    3被称之为首位

    首位必须是在当前进制范围内的数字。例如这个例子中,首位可以是3不能是32。在十进制中首位的范围是0 ~ 9,同理二进制中首位的范围是0 ~ 1。

    2345被称之精确度,也叫有效数字

    这个精确度并不是指一个数字的小数部分,作为一个小于1的数来理解。他所指的精确度是指精确到哪一位(bit)。例如:

      32345,精确到千位,记作: 3.2345 * 10^4 
      32340,精确到百位,记作:3.234 * 10^4 
      32300,精确到十位,记作:3.23 * 10^4 
    

    通过上面的介绍可知以下三点:

    科学计数法主要由四部分所组成:首位精确度(有效数字)进制指数

    影响一个数字大小的关键就在于指数,只要指数够大数组可以成进制的倍数所增长。在指数确定的情况下,影响一个数组的大小就在于有效数字。当有效数字到达所处进制的最大时,则需要指数进位。例如: 3.23451043.2345 * 10^4

    对于一个整数来说,有效数字的位数不能超过指数。 例如3.234561043.23456 * 10^4

    IEEE 754 64 存储格式

    IEEE 754 是一种二进制浮点数算术标准, 规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。

    通常我们用到的比较多的就是单精度(32位)以及双精度(64位)。在强类型语言(Java、C)中单精度浮点数用float表示,双精度浮点数使用double来表示。但是JavaScript是一种弱类型语言,他的number类型只有一种,那就是double双精度类型,即IEEE 754 64。

    以下就是64位存储时,他在计算机内存中 位(bit)的表现形式:

    这张图有以下几点说明:

  • 绿色部分表示指数位。每一个小方格都表示计算机内存中的1位(bit),其中存放着0或1;
  • 计算机内存中从右向左分别是低位到高位,因此最右边的是低位0,最左边的是高位63;
  • 前面提到IEEE 754是以科学计数法来表示的,因此这幅图是他的科学计数法表现类型,但是与上面科学计数法中演示的10进制例子不同,计算机中是使用二进制的科学计数法储存的
  • 对标上方十进制科学计数法的四个组成部分:

    上面提到过,二进制中首位的范围是0 ~ 1。0乘以任何数都等于0,所以他不具有什么代表意义,是一个特殊值。除却0之外,就只剩下1。由于二进制科学计数法的首位只能是1,所以被舍弃掉了,没有在图中的64位中表示出来,而是在计算时自己手动添加。

    由于计算机存储只有二进制,因此在图中也舍弃掉了表示进制的位,在使用时自己手动赋值为2。

    图中的绿色部分就表示科学计数法的指数位,可以看到从第52位开始到第62位结束总共11位表示的是指数位。这里有一个容易混淆的点,这11位表示指数位,并不代表指数位的范围是0 ~ 11。在二进制中,11位二进制表示的范围是[0, 2047]

    这个2047是怎么计算的呢?其实自己简单推导以一下就可以算出来:

    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+11) => 1.F(n个1)  => 1(n+11) * 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);       // 1.7976931348623157e+308
    console.log(a === b); // true
    

    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);       // 5e-324
    console.log(a === b); // false
    

    哪里出了问题???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);       // 5e-324
    console.log(a === b); // true
    

    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);          // Infinity
    console.log(a === b);    // true
    

    这里也回答了上面Number.MAX_VALUE提出的问题,Number.MAX_VALUE表示的是规约数的最大值,却不是JavaScript所能表示的最大值Infinity

    Number.NEGATIVE_INFINITY

    var a = -(2 **1024);
    var b = Number.NEGATIVE_INFINITY;
    console.log(a);        // -Infinity
    console.log(a === b);  // true
    

    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);  // true
    

    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); // true
    

    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!
  • 十进制双精度浮点数与二进制转换的网站
  • 分类:
    前端