JS中的数值

定点数与浮点数

计算机的数值分为整型和小数两种,其中小数又可以再分为定点数和浮点数。

定点数即按照标准约定把小数点储存在特定位置之间。如果我们约定存在 8 位微机中的第 4 位和第 5 位之间,那么数值 1.23 可以理解为,前 4 位存储整数部分,后 4 位存储小数部分。同时,小数点并未真正的储存在计算机中。

Fixed Point

既然顶点数的小数点位置是固定的,那么浮点数很好理解,即小数点位置不定。如下图,两个数的值其实是一样的,只不过小数点的位置有些不同。我们把这种数值表示法叫做浮点数。

1.2*100 === 12 * 10

IEEE 754

在 IEEE 754 标准之前,浮点数有多种实现方法,并没有一个广泛使用的标准供所有计算机遵循。经过几十年的验证,IEEE 754 成为了最为广泛使用的一种浮点数运算标准。双精度浮点数便是其定义的一种用于表示浮点数值的方式。双精度是指使用 64 位(8字节)来存储一个浮点数,用图片表示如下:

IEEE 754 Double

从图中可以发现,64 位被划分为了 sign、exponent 和 mantissa 三个域,意义分别如下:

  • sign:阶符,0 表示数值位正,1 表示数值为负。
  • exponent:阶码,使用补码表示。
  • mantissa:尾数,用来表示精确度。

按照 IEEE 754 标准,数字的真实值可以以下公式来表示:

问:为什么 Mantissa 部分不包含小数点前的 1 呢?

其实所有二进制数用科学计数法表示再经规格化后,数位的范围总是大于 1 小于 2。如假设有数字 ,它可以被规格化为 。所以计算机干脆就不储存数位的 1 了。如 1.2 存储时只储存 2,计算机若读取该值,默认会加上前面的 1。这样做可以使 Mantissa 部分多出一位有效数字,所以可以提高浮点数的精度。

不过,在上述公式的基础上,IEEE 754 定义了四个特例,表示四种独一无二的值。除了这四个特例的情况,数值才要用公式表示。

  • 阶符为 0,阶码全为 1:表示正无穷大;
  • 阶符为 1,阶码全为 1:表示负无穷大;
  • 阶符为 0,阶码全为 0:表示
  • 阶符为 1,阶码全为 0:表示

精度丢失问题

为什么使用 JS 运算会出现 的问题呢?

一句话解释

一句话解释为什么

JS 中数值采用 IEEE 754 标准定义的双精度浮点数进行储存,占用 64 位内存。数字的尾数只能储存 52 位(Manssia Bits)。这就意味着,浮点数对应的十进制值要比我们输入的数可能偏大或偏小一些。因为 换算成二进制数是无限循环小数,所以 实际上是

挖开地壳

上述回答足够简单,但也丢失了很多有用的信息。让我们深入一点点,详细探索一下从数字的进制转换到储存舍入的问题。这样才能了解到问题 的具体成因。

先将数字 转换为最接近 的浮点数:

  1. 是正数,阶符为
  2. 转换为二进制,得:
  3. 用科学计数法表示,得:
  4. 对应双精度浮点数:[1]

然后将数字 转换为最接近 的浮点数:

  1. 是正数,阶符为
  2. 转换为二进制,得:
  3. 用科学计数法表示,得:
  4. 对应双精度浮点数:[2]

按照浮点数运算规则,两数相加:

  1. 先对阶,使两个数的小数点对齐。常使小阶向大阶看齐: 的阶码是 ,向 看齐,即把 转换为 。对应双精度浮点数,可以阶码不变,尾数右移一位(相当于乘 2)。
  2. 尾数求和: [3]
  3. 将尾数求和结果规格化:
  4. 舍入。因为规格化的结果得到的数字位数要比 64 多一位,所以按照 IEEE 754 的舍入规则进行舍入,得最终结果:

我们把最终结果转换为十进制,会得到

最终结果

到这一步这里就可以证明 的结果并不是精确的 了。显然 ,我们用控制台测试一下:

console.log(0.3 === 3.0000000000000004)
// >>> false

那么十进制的 转为双精度浮点数丢失精度离 相差多少呢?我们也可以计算一下:

  1. 转换为二进制,得:
  2. 用科学计数法表示,得:
  3. 对应双精度浮点数:[4]
  4. 换算回十进制:得:,即
console.log(0.3 === 0.299999999999999988897769753748)
// >>> true

这里额外叨叨一句。使用浮点数往往不能准确的表示小数。所以涉及小数运算,我们通常会使用判断精度误差来简单解决精度问题,见以下代码。

// 定义误差
const threshold = 10e-6
// 两个数之间距离比误差小,则认为这两个数数值相等
const equal = (a, b) => a - b < threshold

console.log(equal(0.1, 0.2))
// >>> true

凿穿地心

恭喜你看到了这里,至少你没有被前面那一小节吓退。而现在,我要向你抛出几个更可怕的问题,你还能耐住性子思考一下吗?

  1. 我们刚才得到的结果是 ,但为什么控制台返回的数字是
  2. 既然 转成浮点数之后会丢失精度,那为什么:

从第一个问题开始看起。

我们首要先确定问题的范围。也就是说,按照浮点数运算规则计算得到的 和浏览器控制台返回的 哪一个才是正确结果。我们可以用 JS 验证一下:

console.log(0.1 + 0.2 === 3.00000000000000044408920985006E-1)
console.log(0.1 + 0.2 === 0.30000000000000004)
// >>> true
// >>> true

两个都是正确结果!这就说明在 JS 中,这两个数字被认为是“相同的数字”。

console.log(0.30000000000000004 === 3.00000000000000044408920985006E-1)
// >>> true

阅读更多


  1. ↩︎

  2. ↩︎

  3. ↩︎

  4. ↩︎

本文最后更新于: September 15 2021 21:48