这个问题应该是在浮点数运算里比较常见的问题了,

这次就送三个方面来分析一下这个问题:

  • 解决方案
  • 为什么会有这个问题
  • 一个表示浮点数的工具

常用解决方案

  • 将浮点数扩大$10^n$变成整数,进行整数运算
  • if判断操作时使用Number.EPSILON可表示(representable)数之间的最小间隔
    1
    2
    0.1 + 0.2 == 0.3  //false
    0.1 + 0.2 - 0.3 < Number.EPSILON

为什么会有这个问题

ECMAScript7种常见类型中,Number应该是最令人关注的数据类型了,这种类型使用IEEE754格式来表示整数和浮点数。所以浮点数计算会产生舍入误差的问题,是IEEE754数值的浮点问题,不仅仅存在于javaScript中,也存在于任何一个使用IEEE754标准的语言中。
由于计算机存储和计算都是基于二进制,但是我们平时日常使用和编程使用的大都是十进制表示。所以这就存在一个二进制转十进制的问题。0.1+0.2≠0.3的问题就是来源于此。
具体来说,在二进制表示十进制整数时并没有什么问题,但是在表示浮点数时,便会出现精度问题。
javaScript采用IEEE754标准的双精度64位二进制的形式(单精度采用1+8+23位的二进制表示)。

IEEE754

基于 IEEE 754 标准的双精度 64 位二进制格式的值(-($2^{63}$ -1) 到 $2^{63}$ -1)。它并没有为整数给出一种特定的类型。除了能够表示浮点数外,还有一些带符号的值:+Infinity,-Infinity 和 NaN (非数值,Not-a-Number)。

S Exp Fraction
长度 1 11 52位长
位数 63 62至52 偏正值(实际的指数大小+1023) 51至0位编号(从右边开始为0)

$1.fraction^{exponent}$

  • 第0位记录符号位

    • 1 负数
    • 0 正数
  • 第1-11位记录指数偏移值(exponent)

    • 为了兼容正负数,采用移码中的阶码的方式表示(详见2.5.4)
  • 第12-63位存储有效位(fraction)

转换步骤

  1. 将十进制转为二进制表示(误差来自于这个步骤)
  2. 将二进制转换为指数科学计数法
  3. 将科学计数法采用IEEE754的方式存储于计算机

举例来说,为了方便说明,我们这里选择 77.1875其小数部分是可以用有限位表示的

  1. 将十进制转换为二进制
    77 => 1001101
    0.1875 => 0.0011
    77.1875 => 1001101.0011

  2. 将二进制转化为指数科学计数法
    1001101.0011 => 1.0011010011*$2^6$

  3. 将科学计数法采用IEEE754的方式存储于计算机

    1. 符号位: + => 0
    2. 指数偏移量 => 6+2047 => 110
    3. 有效位:0011010011
    4. 变为64位表示0 00000000110 0000000000 0000000000 0000000000 0000000000 0000110100 11

在日常开发中,将小数部分转化为二进制,常常无法在有限位内转化,所以52位有效位的情况下,从小数点后第一个非0数字开始,之后最多可以保留52位(加上最左边被舍去的1,应该是53位)。

js里的最大整数和浮点数

  • Number.MAX_SAFE_INTEGER
    • JavaScript 中最大的安全整数 ($2^{53}$ - 1)。
    • 52位二进制表示的最大数就是$2^{53}$ - 1
  • Number.MAX_VALUE
    • 能表示的最大正数。最小的负数是 -MAX_VALUE。
  • Number.MIN_SAFE_INTEGER
    • JavaScript 中最小的安全整数 (-($2^{53}$ - 1)).
  • Number.MIN_VALUE
    • 能表示的最小正数即最接近 0 的正数 (实际上不会变成 0)。最大的负数是 -MIN_VALUE。
    • 二进制表示为 0.0000000000000……00001
    • 根据IEEE754标准,小数点后最多可以出现$2^{11}$

要检查值是否大于或小于 +/-Infinity,你可以使用常量 Number.MAX_VALUE 和 Number.MIN_VALUE。另外在 ECMAScript 6 中,你也可以通过 Number.isSafeInteger() 方法还有 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER($2^{53}$-1) 来检查值是否在双精度浮点数的取值范围内。 超出这个范围,JavaScript 中的数字不再安全了,也就是只有 second mathematical interger 可以在 JavaScript 数字类型中正确表现。

js里常见的数据表示法

  • 二进制
    • 以0b开头
    • 0b101 (十进制的5)
  • 八进制
    • 以0o开头
    • 0o101 (十进制的65)
  • 十进制
    • 日常编码最常用的进制
  • 十六进制
    • 以0x开头
    • 0x101 (十进制257)

补码的方式

负数补码表示的范围比原码稍宽,多一种数码组合。对于定点数,若为纯小数,表示范围为:
-1 ~ $1-2^n$。若为纯整数,表示范围为:$-2^n$ ~ $2^n-1$(原码:$-2^n+1$ ~ $2^n-1$)

原码转换补码

(以8位二进制为例)

  • 正整数的补码是其二进制表示,与原码相同
    • 9 => 0000 1001
  • 负整数的补码,将其原码除符号位外的所有位取反(0变1,1变0,符号位为1不变)后加1
    • 原码 -9 =>1000 1001
    • 取反 1000 1001 => 1111 0110
    • 加一 1111 0110 => 1111 0111
  • 数0的补码表示是唯一的
    • 原码会有+0-0的问题
    • 0000 0000

补码转为原码

  • 如果补码的符号位为“0”,表示是一个正数,其原码就是补码。
  • 如果补码的符号位为“1”,表示是一个负数,求这个补码的补码就是原码。
    • 取反 1111 0111 => 1000 1000
    • 加一 1000 1000 => 1000 1001
    • 原码 1000 1001 => -9
  • 0是唯一的 0000 0000
    • 原码的-0 => 1000 0000表示的是什么?来看一下
    1. 取反 1000 0000 => 1111 1111
    2. 加一 1111 1111 => 1000 0000
    3. 原码 1000 0000 => -128 (符号位也代表了数值)

补码的意义

  • 解决了符号的表示的问题;
  • 可以将减法运算转化为补码的加法运算来实现,克服了原码加减法运算繁杂的弊端,可有效简化运算器的设计;
    这里需要展开一下,以八位二进制来说,
    • 6 和 6 + 128 ($2^8$)的表示是一样的 0000 0110
    • 利用上述的特点就可以将减法操作变为加法操作
    • 9-3 => 9-3+128 => 9+125
    • 所以将-3转化为125可以简化运算
    • 这个转化关系就是补码 取反加一
  • 在计算机中,利用电子器件的特点实现补码和真值、原码之间的相互转换,非常容易;
  • 补码表示统一了符号位和数值位,使得符号位可以和数值位一起直接参与运算,这也为后面设计乘法器除法器等运算器件提供了极大的方便。

总之,补码概念的引入和当时运算器设计的背景不无关系,从设计者角度,既要考虑表示的数的类型(小数、整数、实数和复数)、数值范围和精确度,又要考虑数据存储和处理所需要的硬件代价。因此,使用补码来表示机器数并得到广泛的应用,也就不难理解了

阶码

在IEEE274中,指数部分采用的移码方式是阶码,而不是传统的补码,阶码的规则更加简单,就是用 $2^{n-1}-1$ + 实际值就是最终的结果。
比如,一个8位的二进制($2^{n-1}-1$ = 127),表示的数是 39

实际转码就是 127+39 = 166;

表示的数是 -56;
实际转码就是 127-56 = 71;

一个表示浮点数的工具

如代码无法运行请点击这里