likes
comments
collection
share

JavaScript中的数字

作者站长头像
站长
· 阅读数 19

在 JavaScript 中,你可能会对下面的结果感到疑惑:

> 9007199254740992 + 1
9007199254740992,溢出不计

> 9007199254740992 + 2
9007199254740994,结果正常

> 0.1 + 0.2 === 0.3
false

这并不意味着 JavaScript 出错了,更可能的原因是我们对 JavaScript 是如何处理数字的理解不够。

一切数字皆浮点数

  • 数字是数据的一种。
  • JavaScript 使用变量存储数据。number 类型变量存储内容为数字的数据。
  • 与其他语言不同,JavaScript 中的数字都是浮点数。浮点数既可以表示整数,又可以表示小数,JavaScript 使用 64 位二进制存储浮点数(双精度)。

二进制与十进制

先过一遍基础。

二进制转十进制
1010111.11 => 2^6+2^4+2^2+2^0+2^-1+2^-2=55.75

所以,1010111.11 => 55.75
十进制转二进制(整数部分)
连除法:模2为低位。
55 => 1010111

十进制转二进制(小数部分)
连乘法:乘2取高位
0.75*2=1.5 => 1
0.5*2=1.0 => 1

所以,55.75 => 1010111.11

底层表示

JavaScript 的浮点数根据 IEEE 754 标准中的双精度规范来存储:

  • fraction:第0-51 位,共 52 位。取值 0 到2^52。
  • exponent:第52-62 位,共 11 位。取值 0 到 2^11。
  • sign:第 63 位,共一位。

sign 是标志位,表示正负数。因为该符号是独立于数字的且只有正负两种取值,没有额外的缺省值,所以就算是 0 也得取正负值,底层只存在+0 和-0,没有 0。

expoent 是指数位。sign 是为整体服务的,因此 exponent 需要用自己的方式表示正负。具体一点,就是一个数字的指数是正的还是负的,和这个数字是正数还是负数毫无关联,不能用 sign 表示 exponent 的正负,需要 exponent 自己存储,因此,exponent 使用的是偏移二进制,底层的取值 0 到 2^11 表示-1023 到 1024。后面用 e 表示原始数值,p 表示表示的值,即 p = e-1023。

fraction 用于存储实际的数字序列。

数字表示

为方便叙述,下面用%1.f 表示未偏移值,为偏移值*2^p 等于最终数值,百分比表示 f 是以二进制形式表示的。比如当 f=0 ,p=0 时,存储的数字是%1.0*2^0 ,也就是数字 1。这种记法也称规范化表示。

注意,能否使用这种记法和 p 的取值有关:

  • p 取 1024,表示 infinity 和 NaN。f 可以取任意值,不使用规范化记法。
  • p 取-1022 到 1023,表示%1. f 中小数点偏移位置。
  • p 取-1023 时,如果 f=0,表示数字 0,如果 f 为非零值,不能使用规范化记法,要用非规范化记法。

非规范化记法:%0.f*2^C。因为在双精度规范中,非规范记法的 p 只有一种取值 -2022,所以这里先标记为 C 表示常数。

接下来我们就可以用这 64 位比特存储数字了。下面是一些例子:

1.一个零开头的二进制小数,小数点后有52位二进制数。
当p为-2023,f为非零值,表示%0.f * 2^-2022。

2.0
此时f为0,p为-2023

3.1到2^52整数
%1.f * 2^p
f为对应的整数,取1到2^52,而p自己浮动到合适的位置(取值0到52)

例子:
%1.0 * 2^0=1
%1.0 * 2^1=2,即1右移一位
%1.1 * 2^1=3,即1.1右移一位
%1.0 * 2^2=4,即1右移两位
...

4.一些不连续的二进制数

例子:
%1.1 * 2^-1,即1.1左移一位,为0.11(二进制)0.75(十进制)
%1.1 * 2^-100 = 一个非常小的数
%1.1 * 2^100 = 1.901475900342344e+30,一个非常大的数

讨论:

  1. 大数不连续:这里的连续指自然数连续。第四点中,因为 p 可以取-1022 到 1023,所以看起来可以取很大的数,但是因为 f 只有 52 位,所以 p 大于 52 或小于-52 后的数都是不连续的(往前补零或者往后补零),所以只能取特定的大数,比如开头的例子:
> 9007199254740992 + 1
9007199254740992,溢出不计

> 9007199254740992 + 2
9007199254740994,结果正常

9007199254740991 是 53 位的 1,即 %1.f*2^32 中 f 的 52 位都取 1。也就是说,9007199254740991 是单靠 f 就能取到的最大数字。 11111111111111111111111111111111111111111111111111111 52 个 1。

9007199254740992 为 %1.f*2^33,即在 1 位 1 和 32 位 0 后多移动一位补零,是在 f 已经满了的情况下,借助 p 能取到的最大值。在这个数以下的数都可以取到。f 是 10000000000000000000000000000000000000000000000000000 并在 p 的帮助下再右移一位得到 100000000000000000000000000000000000000000000000000000(53 位),即9007199254740992。

而接下来,f 只能+1,得到 10000000000000000000000000000000000000000000000000001 并在 p 的帮助下再右移一位。100000000000000000000000000000000000000000000000000010,取到9007199254740994。

因此要取到9007199254740993,需要 53 位的 f,而系统会舍去末尾的零,溢出不计。

  1. 小数部分不连续:仍是因为 f 最多只有 52 位,所以将 52 位分割成整数和小数两部分,很容易就知道里面存在不能连续的小数。

  2. 第三点中,可以看作是第四点的特例:之所以是整数只是因为小数部分刚好全是零。

  3. 第一点中,只要 p 为-1023,f 不为零,其值就按%0.f * 2^-2022 计算,为什么 C 是-2022,因为可以平滑地转化为规范记法:%1. f* 2^-2023,这样就第四点中,和 p 取 -2022 到 2023 对接上了。因此第一点也可以看作第四点的特例。只不过占用了 f=0 的场景用来表示数字 0。

综上:双精度规范可以表示 -2^33 到 2^33 之间的自然数,以及不连续的小数、不连续的大数。

进制转换的精度丢失

经过上面的分析,我们已经解决了第一个问题,理解了为什么会溢出,这是双精度规范设计的,并不是一种错误。而 0.1+0.2 !== 0.3,确实是一种错误,但这种错误是系统错误,来自高进制小数转低进制小数时必然的精度丢失。所有语言都无法准确地表示一些小数,只是他们的处理方法与 JavaScript 不同。

低进制可以准确转换为高进制,高进制小数转化为低进制时可能丢失精度。或者说,高进制可以准确表达低进制数,低进制数是什么样子的,高进制都可以准确说出来,而高进制的小数是什么样子的,比如 0.1,对于二进制来说,它无法准确理解,也无法准确表达,也就不能准确转换。

哪些小数是无法准确被表示的?像 0.1、0.2 这种,它的分母无法完全被 2 分解,只能计算到溢出。如果是 0.5 这种,可以化为 1/2,二进制就可以准确存储它。

解决这个问题: 在 JavaScript 中,f 是 52 位,不考虑 p=-1023 的情况,那么小数部分连续表示的就是 53 位了:

%1
%1.1
%1.1.....11+52位共53位,配合上p,就有0.xxxxx,xxxxx部分占53

也就是说,一个不能准确表示的数,最多会用连乘法算到二进制的 53 位,比如:

0.7转化二进制:
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.7=0.10110 0110 0110 0110 ...
约为 0.10110011001100110011001100110011001100110011001100111(省去后面的循环)
而这个数是0.7000000000000001
 2^-53 = 0.000000000000000111(十进制)

所以当0.7000000000000001 和 0.7 的误差小于0.000000000000000111 时,可以认定他们在误差允许之内是相等的。

实现和半全等 (==) 类似的效果:

const isEqual=(a,b)=>Math.abs(a-b)<Math.pow(2,-53);
console.log(isEqual(a,b));
console.log(a==b);

参考: segmentfault.com/a/119000001…

转载自:https://juejin.cn/post/7224456722827132984
评论
请登录