文章目录
  1. 1. 思考
  2. 2.
    1. 2.1. 机器码和真值
    2. 2.2. 原码、反码、补码
      1. 2.2.1. 原码
      2. 2.2.2. 反码
      3. 2.2.3. 补码
        1. 2.2.3.1. 补码10000000为什么可以表示-128?
    3. 2.3. IEEE 754标准
    4. 2.4. 标准的基本原理:
    5. 2.5. 回到JavaScript
      1. 2.5.1. ES的”The Number Type”:
      2. 2.5.2. JavaScript不是只有64-bits的双精度
    6. 2.6. 将实数转换成浮点数
      1. 2.6.1. 浮点数的规范化
      2. 2.6.2. 根据精度表示浮点数
        1. 2.6.2.1. 如何把十进制小数转换成二进制小数。
        2. 2.6.2.2. 用二进制科学计算法表示
        3. 2.6.2.3. 表示成 IEEE 754 形式
      3. 2.6.3. 哪些数能精确表示?
      4. 2.6.4. 为啥 0.2+0.4 不等于0.6
    7. 2.7. 特殊值
      1. 2.7.1. NaN(Not a Number)
      2. 2.7.2. 有符号的零

思考

先想几个问题吧:

  1. JavaScript的数字为什么有0和-0?
  2. JavaScript中的NaN为什么互不相等?
  3. JavaScript中的数字真的只有一种类型吗?
  4. JavaScript中常被诟病的0.3 - 0.2 == 0.1原因是什么?
  5. 数组的最大长度是多少?为什么是这个值?

上述问题,只有在JavaScript中有吗?

当下,计算机如此普及,我相信,即便非程序员也了解:计算机的世界只有01。而一个程序员应该了解:0/1组成的东西叫机器码,有原码, 反码, 补码等。而一个JS程序员应该了解:JS中的数字是不分类型的,也就是没有byte/int/float/double等的差异。而一个稍微研究ES规范的JS程序员应该了解:JS的number是IEEE 754标准下64-bits的双精度数值,而且ES中有ToInteger/ToInt32/ToUint32/ToUint16Type Conversion。下面,我们就尝试着讨论一下这些。

从硬件的角度上讲,维护两个状态是相对容易的,比如一个二极管的导通或者截止,一个电脉冲的高或者低,从而在实现集成电路时候可以更加简单高效,所以计算机普遍使用0和1来存储和计算。那么,只有01,如何表示1234567890呢?这就涉及到机器码真值

机器码和真值

  • 所谓机器码是指,整数在计算机中二进制形式。规则很简单,机器码的最高位(左第一位)表示数字的正负,0表示正数,1表示负数,其余位按照进制转换的规则表示具体数字。
  • 所谓真值是指,机器码按照上述转换规则还原的带有正负的实际整数。

举例而言,用8-bits表示一个整数,则十进制的整数+6可表示为:00000110;十进制的数字-5可表示为10000101。这里说的+6和-5便是真值,而表示它们的二进制数便是机器码。再次注意,最高位只用于表示正负,比如10000101的真值是-5而非133,以及我们关于机器码和真值的讨论是基于整数范围的,浮点数在计算机中的存储方式与整数有很大差值,将另作讨论。

有了机器码,我们便可以在计算机中使用机器码存储和计算真值,那么机器码在计算机中是如何计算的呢?

原码、反码、补码

机器码分为多种,主要包括原码、反码、补码、移码等,今天我们主要总结一下前三个,而移码非常简单,且多用于比较,不做详细说明。另外需要补充一点,我们在此区分机器码的这么多种形式,主要是针对的有符号数,而无符号数,不需要使用最高位来表示正负,也就不需要这么多种编码方式。

原码

最高位表示正负,其它位表示真值的绝对值。其中,最高位为0表示正数或者0,为1表示负数。
比如,同样以8bits长度的数串表示+7的原码为0000 0111,-7的原码为10000111。以后,我们会这样表示:

1
2
[+7] = [00000111]原
[-7] = [10000111]原

很明显,8-bits的原码能记录的范围为:[-127,+127]
原码的好处在于,易于理解,相对直观,方便人脑识别和计算。
对于原码,人脑使用,可以直接计算出其真值然后可以进行后续操作。但对于计算机,
首先,因为最高位用于表示正负,所以不能直接参与运算,需要识别然后做特殊处理;
其次,具体计算使用绝对值进行操作,所以两个操作数正负的异同会影响操作符,比如两个异号相加实际要做减法操作,甚至异号相减还需要判断绝对值大小然后决定结果正负。
如此,我们计算机的运算器设计将会变得异常复杂。下面,我们将了解如何使用反码和补码将符号位参与运算,从而使加减法统一简单高效地处理,这也是反码和补码出现的原因。

反码

正数的反码等于其原码,而负数的反码则是对其原码进行符号位不变,其它位逐一取反的结果
比如,同样以8-bits长度的数串表示+7,那么有如下:

1
2
[+7] = [00000111]原 = [00000111]反
[-7] = [10000111]原 = [11111000]反

同样,8-bits的反码能记录的范围为:[-127,+127]
在按位取反之后,我们可以有下面的操作:

1
2
3
4
5
6
2 - 3 = 2 + (-3) 
= [00000010]原 + [10000011]原
= [00000010]反 + [11111100]反
= [11111110]反
= [10000001]原
= -1

上面,我们将减法通过反码转化为了加法,如此,我们的运算将会简单很多,但是反码的方式同样存在一些问题:

1
2
3
4
5
6
3 - 3 = 3 + (-3) 
= [00000011]原 + [10000011]原
= [00000011]反 + [11111100]反
= [11111111]反
= [10000000]原
= -0

出现了-0,这个值是没有意义的。另外,按照反码加法法则,如果最高位有进位,需要在最低位上+1,那么会出现:

1
2
3
4
5
6
3 - 2 = 3 + (-2) 
= [00000011]原 + [10000010]原
= [00000011]反 + [11111101]反 (这里最高位有进位,需要在最低位+1)
= [00000001]反
= [00000001]原
= 1

这种情况,又增加了反码运算的复杂性,影响效率,为解决上面的问题,出现了补码。

补码

正数的补码等于其原码,而负数的补码则是对其反码进行末位加1的结果。

1
2
[+7] = [00000111]原 = [00000111]反 = [00000111]补
[-7] = [10000111]原 = [11111000]反 = [11111001]补

使用补码,继续做之前的操作:

1
2
3
4
5
6
7
8
2 - 3 = 2 + (-3) 
= [00000010]原 + [10000011]原
= [00000010]反 + [11111100]反
= [00000010]补 + [11111101]补
= [11111111]补
= [11111110]反
= [10000001]原
= -1

那么,如果是3-3呢?

1
2
3
4
5
6
7
3 - 3 = 3 + (-3) 
= [00000011]原 + [10000011]原
= [00000011]反 + [11111100]反
= [00000011]补 + [11111101]补
= [00000000]补
= [00000000]原
= 0

是否还需要做额外的加法操作?

1
2
3
4
5
6
7
3 - 2 = 3 + (-2) 
= [00000011]原 + [10000010]原
= [00000011]反 + [11111101]反
= [00000011]补 + [11111110]补
= [00000001]补
= [00000001]原
= 1

这样,我们便可以完美的将减法统一到加法之上,而且不需要繁琐的正负判断,进位控制,甚至可以节约一个位置。
那么,这个位置,也就是10000000如何处理呢?
按照规定,10000000用来表示-128,正数的补码/反码/原码相同,而负数的补码只是占用了-0的[10000000]原[11111111]反转换后得到的[10000000]补表示-128,但是这个只是帮助理解,不能反向回推得到-128的原码和补码。
所以,8bits的补码能记录的范围为:[-128,+127]
至此,我们已经了解了,计算机中主要使用的存储和计算整数的方式,鉴于现代计算机主要使用补码方式,自然能很容易理解各种数字类型的表示范围,比如32bits的int范围为:[-231,231-1]。这对于我们后面理解一些JavaScript中的极端情况至关重要。

补码10000000为什么可以表示-128?

有知乎大神这么理解:
作者:fhylhl 链接:http://www.zhihu.com/question/28685048/answer/41735701 来源:知乎

很多人并不理解补码。补码就是同余啊。1000000是正128你知道吧,正负128模256是同余的。加减乘可以直接算也是同余的定理决定的,而不是凑出来的巧合,哪可能凑出这种东西?
8位只能表示256个数,0到255,但我还想表示一些负数怎么办呢?就用与该负数同余的正数来表示呗。-1=255,-2=254,等等。
建议脱离算数的思维方式,这其实就是一个环。模任何一个正整数(如256),可以把所有整数分类,比如模256可分256类,0 256 -256…是一类(余0类),1 257 -255…是一类(余1类),等等,这256类可看作环的元素,你看-128和128是同一个类里的(余128类),用一个代表另一个罢了。补码和普通的unsigned integers都是在每类中选一个数,unsigned integers选0到255,补码表示的有符号整数选-128到127,都是一个数恰好对应一个类。当你明白这一切后,补码就是顺理成章的事。

同余:数论中的重要概念。给定一个正整数m,如果两个整数a和b满足(a-b)能够整除m,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,记作a≡b(mod m)。对模m同余是整数的一个等价关系。

IEEE 754标准

作为一个JavaScript程序员,我们只有一个Number,所以我们从一开始就习惯了:

1
2
var num1 = 123;
var num2 = 1.23;

但是,你知道JS的number是IEEE 754标准的64-bits的双精度数值吗?这是一个什么样的标准?使用这个标准的64-bits双精度意味着什么?所以,要掌握JavaScript中的数字,我们首先得了解IEEE 754标准。下面,我将尝试说明一下这个标准,为我们最后学习JavaScript中的数字做铺垫。

标准的基本原理:

我们知道,对于计算机而言,数字没有小数和整数的差别,也就是计算机中没有小数点的存在。通过前文的讨论,我们已经找到了很完美的整数存储计算的方案,但是当涉及到小数,我们很容易发现,现有的方案无法解决我们的需求。然后,计算机科学家们便尝试了多种方案,主要便是定点数浮点数两种。

  • 所谓定点数,是指小数点位置固定在数串中间的某个特定位置,点两侧分别为数字的整数和小数部分。比如用8-bits字长的数串,小数点固定在正中间位置,那么1100100100110101分别表示1100.100111.0101两个数字。这种方案简单直观易理解,但是存在严重的空间浪费,以及容易溢出的问题。
  • 所谓浮点数,是指小数点的位置是不固定的,通过科学计数法(这个应该不需要解释吧)的方式控制小数点的位置,表示不同的数字。这个表示方案便是IEEE 754标准使用的方案。IEEE 754标准是目前使用最广泛的浮点数运算标准。下面我们将主要讨论一下此方案。

现在,让我们想一下小时候学习的科学计数法,比如-123.456这个数字,转换成科学计数法应该是:-1.23456 × 10^2。这里面已经包含了IEEE 754标准的主要元素。我们梳理一下:第一个,自然是正负号的问题,需要一个标志;然后,需要一个具体的数字,表示有效数字或者精度,如上例的1.23456;再然后,需要一个控制小数点位置的数字,如上例的10^2,回忆一下,我们学习科学计数法的时候,要求前面的数字的绝对值大于1而小于10,也就是小于10^2中的底数(Base),进制固定之后,底数应该是固定的,所以这里起决定作用的是指数,也就是上例中的2。那么,有了这三个元素,我们便可以很轻松的表示出一个数字,并且灵活的调节小数点位置从而控制数字正负、精度和大小。

上面的要素,转换成标准语言描述,我们称表示正负的标志叫符号(Sign),表示精度的数字为尾数(Mantissa)或者有效数字(Significand),而控制小数点位置的指数就叫指数(Exponent),指数和基数(Base)共同作用参与计算。下图取自wikipedia,我们直观地感受下这三个要素在一个数串中的相对关系(fraction区域即等同于前面说的有效数字区域):
123.png
了解最基本的原理后,我们来大致看一下IEEE 754标准做了什么。
首先做的事情就是规定这三个要素在一个数串中占有的位数,试想一下,如果各个实现的位数不确定,那么我们是不是很难正确的还原出原始数字?IEEE 754标准规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80比特实做)。只有32位模式有强制要求,其他都是选择性的。而现在主流的语言,多提供了单精度和双精度的实现,我们在此主要比较一下这两者,如图是它们各个部分对应上图,所使用的位数如下:


补充一点的是,无论是科学计数法还是标准的规定,都要求有效数字(不考虑符号位)必须>=1 && <Base。所以,有效数字其实是一个定点数,小数点的位置固定在有效数字域的最高位和次高位之间。那么,按照上述规定,在二进制中,最高位只能是1,所以标准要求省略其最高位,于是精度提高一位。比如,32-bits的单精度有效数字区域只有23位,但是精度却是24位;64-bits的双精度,拥有52位的有效数字域却是53位精度的。

然后,还有一个问题,如果按照先有的约定,是不是无法表示小于1的实数?因为,指数一定>=0,有效数字一定>1。于是,IEEE 754标准提出了一个很重要的指数偏移值。它是说明指数域(Exponent占用的区域)的编码值为指数的实际值加上某个固定的值,换言之便是,如果我们根据指数域计算出的指数是N,那么参与计算实际浮点数的指数应该是N-指数偏移值。根据IEEE 754标准的规定,该固定值为2^(e-1) - 1,其中的e为存储指数的比特的长度。比如,从上图中我们看到,32-bits的单精度是以8-bits表示一个指数域,那么偏移值应该是2^(8-1) - 1 = 128−1 = 127。所以,容易得出,单精度浮点数的指数部分实际取值是全零00000000 0-127=-127全零11111111 256-127=128就是[-127,128]。比如,某个32-bits单精度的指数为十进制的1,那么指数域的编码应该是10000001,某个32-bits单精度的指数域编码是00000001,那么该指数的实际值应该是十进制的-126。这样,我们就能通过偏移值将正指数转换为负指数,从而使浮点数能逼近0。浮点数的指数计算跟前面讨论的机器码恰好相反,正数的最高位都是1,而负数的最高位都是0

以上的描述,便是IEEE 754标准最需要我们了解的原理部分,但是,作为一个广泛使用的工业标准,规定这些还是远远不够的。
wikipedia(维基百科)对IEEE 754标准有如下描述:

这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。

下面,补充几个,我认为与本文后续讨论相关的或者可以帮助大家理解极端现象的定义:
规约形式的浮点数:如果浮点数中指数部分的编码值在0 < exponent < 2^(e-1)之间,且尾数部分最高有效位(即整数)是1,那么这个浮点数将被称为规约形式的浮点数。也就是,严格按照我们上文描述编码的数字。
非规约形式的浮点数:如果浮点数的指数部分的编码值是0,尾数为非零,那么这个浮点数将被称为非规约形式的浮点数。IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值大1。例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0
上面的两个概念,几乎是直接从wikipedia上扒下来的,·非规约形式的浮点数·出现的意义是避免突然式下溢出(abrupt underflow),而采用渐进式下溢出。这已经是上世纪70年代的事情了,差不多是我的年龄的两倍了。这个是一些非常极端的情况,在此我尝试最简单地描述一下非规约形式的浮点数出现的意义,知道有这么回事便可:下面,以单精度为例,如果没有非规约形式的浮点数,那么绝对值最小的两个相邻的浮点数之间的差值将是绝对值最小的浮点数的2^23分之一,大家想一下,绝对值次小的浮点数减去绝对值最小的浮点数的值是多少?

1
2
3
1.00...01 × 2^(-126) - 1.00...00 × 2^(-126) = 0.00..01 × 2^(-126) 
= 1 × 2^(-126-23)
= 2^(-149)

很明显,绝对值最小的规约数无法表达其和次小的规约数的差值,所以很容易导致有若干数字之间的差值下溢,可能会触发意料之外的后果。而如果采用非规约形式的浮点数,指数全0,偏移值比规约数偏移值大1(-126-127大1),尾数小于1,那么非规约数能表达的最小值便是:

1
2
0.00..01 × 2^(-126) = 1 × 2^(-126-23)
= 2^(-149)

所以,非规约形式的浮点数解决了前述的突然式下溢出(abrupt underflow)而被标准采纳。
IEEE 754标准还规定了三个特殊值:

  • 指数全0且尾数小数部分全0,则这个数字为±0。(符号位决定正负)
  • 指数为2^e - 1且尾数的小数部分全0,这个数字是±∞。(符号位决定正负)
  • 指数为2^e - 1且尾数的小数部分非0,这个数字是NaN。
    结合前面的规约数,非规约数以及三个特殊值,可以得到如下总结:10.png 现在,让我们回忆一下,各种语言中普遍描述的双精度浮点数的范围:[-1.7 × 10^(-308),1.7 × 10^308]。打个岔,想象一个有300多位的十进制数字的适用情形,私以为远超过普通人想象力的边界。这个范围为什么是这个范围呢?我觉得,通过上面的讨论,大家应该能清晰,1.7/308这些数字出现的必然原因。
    首先,我们应该很容易根据偏移量得出双精度浮点数的计算公式:11.png 然后,以正数为例,按照上述特殊值中±∞NaN的约定,指数的最大值应该满足指数取规约数的指数范围的最大值,然后小数部分取小数部分的最大值,可以得出这个二进制的数字应该是:
    0 11111111110 11..11(52个)
    转换为16进制表示:
    0x7fef ffff ffff ffff
    那么,根据前述规约数的原理,反编码便得到十进制的:1.7976931348623157 x 10^308。类似的道理,Sign位取反,便是范围的下限。
    到此为止吧,我对IEEE 754标准也是最近几天稍加学习,再说多了就误导大家了。通过这几天的学习,我感觉,我们在理解的IEEE 754标准及浮点数的时候,要特别注意将精度和范围两个概念分别开来。范围只是一个模糊的界限,精度才是能准确表达的数字。

回到JavaScript

在上面的讨论中,我们很少提及JavaScript,似乎有点背离今天的主题了,但是,在了解了前述的原理之后,我们对JavaScript中数字的把握将”水到渠成”。这终将是一次,铺垫多于正文,开胃菜多于正餐的讨论。嗯,快喊小伙伴,正餐开始了!

ES的”The Number Type”:

现在,我们打开ES规范的The Number Type 是不是基本通读下来了?
比如:

The Number type has exactly 18437736874454810627 (that is, 2^64 − 2^53 + 3) values…
为什么是这个数字?因为,我们说JavaScript中的数字是64-bits的双精度,所以首先有2^64中可能的组合,然后,按照前述的IEEE 754标准的标准中的特殊值中的部分,NaN±∞占用了2^53个数值,但是表示了三个直观的量,所以,加减一下,自然就是18437736874454810627 (that is, 2^64 − 2^53 + 3) values。
…the 9007199254740990 (that is, 2^53−2) distinct “Not-a-Number” values…
为什么这么多NaN?同样,按照前述的IEEE 754标准的标准中的特殊值中的部分,NaN使用了Significand非零指数是特定2^e-1且Sign无要求的所有可能,即2^53减去±∞两种情况。
…e is an integer ranging from −1074 to 971…
为什么指数的范围是这个呢?而不是-1022到+1022呢?因为,ES演化了一下公式,对比一下我们之前演示64-bits的公式,关于参与计算的mantissa,我们按照IEEE 754标准在演示的时候中使用的是1.m,而ES规范中使用的是m,当然会有尾数域bit长度的差异了。

到这里,关于数字,大概就可以结束了。开篇的几个问题,相信读到这里的同学,都能有答案了。但是,还有一个问题,JavaScript中的数字真的只有一种类型吗?,而且貌似到现在与我们的初衷,理解>>>有点偏离了。不过,世界上很多事情往往都是这样,解释原理需要到口干舌燥,而用原理去解释现象却只需要三言两语。

JavaScript不是只有64-bits的双精度

是的,小标题已经回答了我们的问题,JavaScript不是只有64-bits的双精度。我们通篇都在说JavaScript中数字的各种,一直按照64-bits的双精度来描述,但是,如之前所说,ES中有ToInteger/ToInt32/ToUint32/ToUint16等Type Conversion。这些Type Conversion不是我们直接调用的API,而是语言引擎在进行某些特定操作的时候,替我们做的。这种“隐形的操作”,只有在一些极端的情况下,会表现出来。现在,我们可以到“ToInt32”/“ToUint32”/“ToInt16”三个地方看一下,稍作比较便能发现,他们的差异很小,只是在特定的步骤中存在差异。比如,ToUint32和ToUint16的差异仅仅操作的最后一步存在差异,按顺序列出比较一下:

Let int32bit be posInt modulo 2^32; that is, a finite integer value k of Number type with positive sign and less than 2^32 in magnitude such that the > mathematical difference of posInt and k is mathematically an integer multiple of 2^32.

Return int32bit.
vs
Let int16bit be posInt modulo 2^16; that is, a finite integer value k of Number type with positive sign and less than 2^16 in magnitude such that the mathematical difference of posInt and k is mathematically an integer multiple of 2^16.

Return int16bit.

比较一下,不难发现,仅仅是2^322^16的差异,而关键点恰是modulo操作的时候,按照我们之前讨论的原理,很容易理解这个操作决定了可能出现的最大数。这样的比较,有一好处,能提高我们阅读标准的速度,而且加深理解,对掌握标准很有帮助。
总结一下这三个操作的范围:

  • ToInt32的范围便是其它强类型语言中的[-2^31, -2^31 - 1]。

  • ToUint32的范围便是其它强类型语言中的[0, -2^32 - 1]。

  • ToUint16的范围便是其它强类型语言中的[0, -2^16 - 1]。
    通过搜索,很容易能找到,JavaScript中那些操作中使用了上述相关的操作。其中,ToUint16仅仅在String.fromCharCode中有使用,我们不做讨论了。ToInt32有在多个位运算符中使用,比如~ / << / >>,以及在parseInt也有使用。而ToUint32的使用则出现在了大量的地方,主要分布在,数组相关的操作,位运算的操作两个区域。
    我们就借ToUint32的这些使用,回到开篇讨论的那个地方吧:
    首先,来到这里>>>,看到操作如下:

    1. Let lref be the result of evaluating ShiftExpression.

    2. Let lval be GetValue(lref).

    3. Let rref be the result of evaluating AdditiveExpression.

    4. Let rval be GetValue(rref).

    5. Let lnum be ToUint32(lval).

    6. Let rnum be ToUint32(rval).

    7. Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
      Return the result of performing a zero-filling right shift of lnum by shiftCount bits. Vacated bits are filled with zero. The result is an unsigned 32-bit integer.

    再看new Array (len),有一句:
    If the argument len is a Number and ToUint32(len) is equal to len, then the length property of the newly constructed object is set to ToUint32(len). If the argument len is a Number and ToUint32(len) is not equal to len, a RangeError exception is thrown.
    对比不难发现,>>>的返回值和array.length的取值范围,无差异,经过>>>操作后的数字,一定是一个合法的array.length。解释原理总是那么复杂,可是用原理解释现象总是那么简单。

将实数转换成浮点数

浮点数的规范化

同样的数值可以有多种浮点数表达方式,比如上面例子中的 123.45 可以表达为 12.345 × 101,0.12345 × 103 或者 1.2345 × 102。因为这种多样性,有必要对其加以规范化以达到统一表达的目标。规范的(Normalized)浮点数表达方式具有如下形式:±d.dd...d × βe , (0 ≤ d i < β)
其中 d.dd…d 即尾数,β 为基数,e 为指数。尾数中数字的个数称为精度,在本文中用 p 来表示。每个数字 d 介于 0 和基数之间,包括 0。小数点左侧的数字不为 0。
基于规范表达的浮点数对应的具体值可由下面的表达式计算而得:±(d 0 + d 1β-1 + ... + d p-1β-(p-1))βe , (0 ≤ d i < β)
对于十进制的浮点数,即基数 β 等于 10 的浮点数而言,上面的表达式非常容易理解,也很直白。计算机内部的数值表达是基于二进制的。从上面的表达式,我们可以知道,二进制数同样可以有小数点,也同样具有类似于十进制的表达方式。只是此时 β 等于 2,而每个数字 d 只能在 0 和 1 之间取值。比如二进制数 1001.101 相当于 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20 + 1 × 2-1 + 0 × 2-2 + 1 × 2-3,对应于十进制的 9.625。其规范浮点数表达为 1.001101 × 23

根据精度表示浮点数

问:要把小数装入计算机,总共分几步?你猜对了,3 步。

  • 第一步:转换成二进制。
  • 第二步:用二进制科学计算法表示。
  • 第三步:表示成 IEEE 754 形式。

如何把十进制小数转换成二进制小数。

十进制小数转换成二进制小数采用”乘2取整,顺序排列”法。
具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。

  • 如:25.7=(11001.10110011001100110011001100110011…)B
    • 整数部分:
      25%2 = 1 ======== 1
      12%2 = 0 ======== 0
      6%2  = 3 ======== 0
      3%2  = 1 ======== 1
      1%2  = 1 ======== 1
      
    • 小数部分:
      0.7*2=1.4========取出整数部分1 
      0.4*2=0.8========取出整数部分0 
      0.8*2=1.6========取出整数部分1 
      0.6*2=1.2========取出整数部分1 
      0.2*2=0.4========取出整数部分0  
      0.4*2=0.8========取出整数部分0 
      0.8*2=1.6========取出整数部分1 
      0.6*2=1.2========取出整数部分1 
      0.2*2=0.4========取出整数部分0 
      
    • 最后结果就是:
      11001.1 0110 0110 0110 0110 0110 0110 0110 0110 0110...
      

用二进制科学计算法表示

11001.101100110 == 1.1001101100110 * 24

表示成 IEEE 754 形式

  1. 正数 固符号位为 0
  2. 尾数 由于第一位使用是1,固取(首位1干掉了).10011 0110 0110 0110 0110 0110 0110 0110 0110 0110
  3. 指数 4 + 1023(偏移量),转换为 二进制就是 10000000011
    组合在一起就是 0 10000000011 10011 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 011

单精度32位的 偏移量是 Math.pow(2,8)/2 -1 == 127 ,双精度64位的偏移量是 Math.pow(2,11)/2 -1 == 1023

哪些数能精确表示?

那么 0.1 在计算机中可以精确表示吗?答案是出人意料的, 不能。

在此之前,先思考个问题: 在 0.1 到 0.9 的 9 个小数中,有多少可以用二进制精确表示呢?

我们按照乘以 2 取整数位的方法,把 0.1 表示为二进制(我假设那些不会进制转换的同学已经补习完了):

(1) 0.1 x 2 = 0.2 取整数位 0 得 0.0
(2) 0.2 x 2 = 0.4 取整数位 0 得 0.00
(3) 0.4 x 2 = 0.8 取整数位 0 得 0.000
(4) 0.8 x 2 = 1.6 取整数位 1 得 0.0001
(5) 0.6 x 2 = 0.2 取整数位 1 得 0.00011
(6) 0.2 x 2 = 0.4 取整数位 0 得 0.000110
(7) 0.4 x 2 = 0.8 取整数位 0 得 0.0001100
(8) 0.8 x 2 = 1.6 取整数位 1 得 0.00011001
(9) 0.6 x 2 = 1.2 取整数位 1 得 0.000110011
(n) …
我们得到一个无限循环的二进制小数 0.000110011…

我为什么要把这个计算过程这么详细的写出来呢?就是为了让你看,多看几遍,再多看几遍,继续看… 还没看出来,好吧,把眼睛揉一下,我提示你,把第一行去掉,从 (2) 开始看,看到 (6),对比一下 (2) 和 (6)。 然后把前两行去掉,从 (3) 开始看…
明白了吧,0.2、0.4、0.6、0.8 都不能精确的表示为二进制小数。 难以置信,这可是所有的偶数啊!那奇数呢? 答案就是:
0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确的表示。
如果把 0.0 再算上,那么就有两个数可以精确表示,一个奇数 0.5,一个偶数 0.0。

那么到底怎么确定一个数能否精确表示呢?还是回到我们熟悉的十进制分数。

1/2、5/9、34/25 哪些可以写成有限小数?把一个分数化到最简(分子分母无公约数),如果分母的因式分解只有 2 和 5,那么就可以写成有限小数,否则就是无限循环小数。为什么是 2 和 5 呢?因为他们是 10 的因子 10 = 2 x 5。二进制和十六进制呢?他们的因子只有 2,所以十六进制只是二进制的一种简写形式,它的精度和二进制一样。
如果一个十进制数可以用二进制精确表示,那么它的最后一位肯定是 5。备注:这是个必要条件,而不是充分条件。

为啥 0.2+0.4 不等于0.6

1
0.2 + 0.4 //0.6000000000000001

1.6 + 2.8 = 4.4
四舍五入得到 4。我们用另一种方法
先把 1.6 四舍五入为 2
再把 2.8 四舍五入为 3
最后求和 2 + 3 = 5
通过两种运算,我们得到了两个结果 4 和 5。同理,在我们的浮点数运算中,参与运算的两个数 0.2 和 0.4 精度已经丢失了,所以他们求和的结果已经不是 0.6 了。

特殊值

  • 指数域不全为0或不全为1。这时,浮点数就采用上面的规则表示,即指数的计算值减去127(或1023),得到真实值,再将尾数前加上第一位的1。
  • 指数域全为0。这时,浮点数的指数等于1-127(或者1-1023),尾数不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
  • 指数域全为1。这时,如果尾数全为0,表示±无穷大(正负取决于符号位s);如果尾数不全为0,表示这个数不是一个数(NaN)。

NaN(Not a Number)

NaN 用于处理计算中出现的错误情况,比如 0.0 除以 0.0 或者求负数的平方根。
NaN 指数域全为 1,且尾数域不等于零的浮点数。IEEE 标准没有要求具体的尾数域,所以 NaN 实际上不是一个,而是一族。不同的实现可以自由选择尾数域的值来表达 NaN。
任何有 NaN 作为操作数的操作也将产生 NaN。用特殊的 NaN 来表达上述运算错误的意义在于避免了因这些错误而导致运算的不必要的终止。回顾我们对 NaN 的介绍,当零除以零时得到的结果不是无穷而是 NaN 。原因不难理解,当除数和被除数都逼近于零时,其商可能为任何值,所以 IEEE 标准决定此时用 NaN 作为商比较合适。

有符号的零

因为 IEEE 标准的浮点数格式中,小数点左侧的 1 是隐藏的,而零显然需要尾数必须是零。所以,零也就无法直接用这种格式表达而只能特殊处理。
实际上,零保存为尾数域为全为 0,指数域为 emin - 1 = -127,也就是说指数域也全为 0。考虑到符号域的作用,所以存在着两个零,即 +0 和 -0。不同于正负无穷之间是有序的,IEEE 标准规定正负零是相等的。
零有正负之分,的确非常容易让人困惑。这一点是基于数值分析的多种考虑,经利弊权衡后形成的结果。有符号的零可以避免运算中,特别是涉及无穷的运算中,符号信息的丢失。举例而言,如果零无符号,则等式 1/(1/x) = x 当x = ±∞ 时不再成立。原因是如果零无符号,1 和正负无穷的比值为同一个零,然后 1 与 0 的比值为正无穷,符号没有了。解决这个问题,除非无穷也没有符号。但是无穷的符号表达了上溢发生在数轴的哪一侧,这个信息显然是不能不要的。零有符号也造成了其它问题,比如当 x=y 时,等式1/x = 1/y 在 x 和 y 分别为 +0 和 -0 时,两端分别为正无穷和负无穷而不再成立。当然,解决这个问题的另一个思路是和无穷一样,规定零也是有序的。但是,如果零是有序的,则即使 if (x==0) 这样简单的判断也由于 x 可能是 ±0 而变得不确定了。两害取其轻者,零还是无序的好。

注: 本文转自 随心小筑 我自己做了本分填充和整理

文章目录
  1. 1. 思考
  2. 2.
    1. 2.1. 机器码和真值
    2. 2.2. 原码、反码、补码
      1. 2.2.1. 原码
      2. 2.2.2. 反码
      3. 2.2.3. 补码
        1. 2.2.3.1. 补码10000000为什么可以表示-128?
    3. 2.3. IEEE 754标准
    4. 2.4. 标准的基本原理:
    5. 2.5. 回到JavaScript
      1. 2.5.1. ES的”The Number Type”:
      2. 2.5.2. JavaScript不是只有64-bits的双精度
    6. 2.6. 将实数转换成浮点数
      1. 2.6.1. 浮点数的规范化
      2. 2.6.2. 根据精度表示浮点数
        1. 2.6.2.1. 如何把十进制小数转换成二进制小数。
        2. 2.6.2.2. 用二进制科学计算法表示
        3. 2.6.2.3. 表示成 IEEE 754 形式
      3. 2.6.3. 哪些数能精确表示?
      4. 2.6.4. 为啥 0.2+0.4 不等于0.6
    7. 2.7. 特殊值
      1. 2.7.1. NaN(Not a Number)
      2. 2.7.2. 有符号的零