JavaScript中最大的数有多大

写在前面

之前有前端同学问我,JavaScript中最大的数有多大。
那时就想写一些文章,从整型各种进制的转换,到原码反码补码的形式,最后到浮点数,再加上字符串类型的数字,把计算机世界里与数字相关的内容都说一说。
但由于对相关知识的掌握程度,表达能力,执行力等各方面原因,一直没有动笔。。

今天看到draveness大佬写了一篇《为什么 0.1 + 0.2 = 0.300000004 · Why’s THE Design?》,很好的讲解了浮点数的知识。

所以本文就直接在大佬文章的基础之上讲解就好了。看本文前可以先看看大佬的文章。

正文

JS中Number类型的数字,不管是整数还是小数,底层都使用64位的浮点数形式存储。所以JavaScript中最大的数有多大,等价于64位浮点数最大的数有多大。

浮点数要解决的问题

我们先跳出实现细节,来谈谈为什么浮点数存在精度问题。

比如0~100这个范围内,整数的个数是有限的,就是101个。
而如果是小数,由于小数点之后的部分是无限的,比如我随便说两个小数,1.23041.2300004,中间到底出现多少个0都是合法的小数,所以理论上是没办法使用有限的存储空间(比如64位)表示完所有的小数。

你可能很容易想到,限制小数点后的位数,比如最多两位,也即范围是0.00~0.99,那么0~100范围内的数就变回有限了,也即101*100=10100个。
这种方式适用一些场景,比如人民币,如果单位是元,那么小数只需要两位,分别是角和分。
可惜的是,并不是所有场景,小数点后保留两位就够用,关于这点相信也不用我过多举例,拿数字3.1415来说,只能存储为3.143.15,也即精度丢失了。
并且,如果总是预留一部分空间存储两位小数,那么也是一种浪费。

抽象来看,我们面临的问题实际上是,如何用有限的空间存储尽量大的数字范围,以及尽量高的精度。
某种角度,浮点数是一种解决上述问题的编码方式。

浮点数的原理

JS和大多数编程语言一样,采用IEEE 754浮点数标准。

在draveness的文章中,图文并茂的对该标准进行了描述,并分别举了0.10.20.15625的例子。建议先看看那篇文章。

浮点数的公式是sign * power(2, exp) * (1 + fraction)

对于32位浮点数,sign占1位,exp占8位,fraction占23位:

  • sign占1位,没什么好说的,浮点数都是有符号类型,该位为0时,是正数,也即公式中的sign为1。该位为1时,是负数,也即公式中的sign为-1
  • exp占8位,总共可表示256个数字,范围是[0, 255],0和255有特殊用途,我们不展开讲,那么还剩下[1, 254],由于浮点数除了支持特别大的数,还要取倒数用于支持特别小的数,所以exp有正有负,这8位的[1, 254]会平移映射成[-126, 127]的exp
  • fraction占23位,这23位中不为0的位就要加上1/power(2, index),index从左到右取值为[1, 23],计算得到公式中的fraction

我们补充看一些正整数的例子加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 -> 1 * power(2, 0) * 1
2 -> 1 * power(2, 1) * 1
3 -> 1 * power(2, 1) * (1 + 1/power(2, 1))
4 -> 1 * power(2, 2) * 1
5 -> 1 * power(2, 2) * (1 + 1/power(2, 2))
6 -> 1 * power(2, 2) * (1 + 1/power(2, 1))
7 -> 1 * power(2, 2) * (1 + 1/power(2, 1) + 1/power(2, 2))
8 -> 1 * power(2, 3) * 1

1 -> 0 01111111 00000000000000000000000
二进制01111111 = 十进制127,平移后得到exp = 0
fraction = 0

7 -> 0 10000001 11000000000000000000000
二进制10000001 = 十进制129,平移后得到exp = 2
fraction前两位有值,所以是1/power(2, 1) + 1/power(2, 2)

这个非常棒的网站中,你可以输入任意数字,查看对应的32位浮点数是如何表示的。

浮点数的范围

回到JavaScript中最大的数有多大这个问题,这其实包含两个问题:

  1. JavaScript Number类型中,最大的那个正整数是多少(也即超过这个数就没法表示了)
  2. JavaScript Number类型能保证精度的正整数范围是多少(也即该范围内的正整数是可完整连续表示的)

听着有点拗口,举个例子就明白了。假设某种表示方式只能存储1, 2, 3, 100这4个正整数,那么第一个问题是100,第二个问题是3。

由于32位和64位浮点数的算法部分是一样的,大部分资料为了简洁,都采用32位讲解浮点数。
我们回到JS中的Number类型,底层使用的是64位浮点数,其中11位是指数部分,52位是小数部分。

指数部分11位,总共可表示2048个数字,范围是[0, 2047],刨去0和2047,剩下[1, 2046],再映射成[-1022, 1023]

对于问题一,指数部分和小数部分都取最大值,即

power(2, 1023) * (1 + 1/power(2, 1) + 1/power(2, 2) + ... + 1/power(2, 51) + 1/power(2, 52)),结果会接近power(2, 1024)

注意,这里由于1023大于52,所以exp和fraction可以都取最大值,计算后的结果依然是整数。

对于问题二,实际上是受小数部分影响,即exp取52,fraction取最大值,也即

power(2, 53) - 1,结果为9007199254740991,这个数字有16位。

另外,JS中定义了一个常量Number.MAX_SAFE_INTEGER,它的值就是9007199254740991

最后,我们再拿JS做个试验,验证下:

1
2
3
4
5
6
7
8
> console.log(Number.MAX_SAFE_INTEGER)
9007199254740991
> console.log(Number.MAX_SAFE_INTEGER+1)
9007199254740992
> console.log(Number.MAX_SAFE_INTEGER+2)
9007199254740992
> console.log(Number.MAX_SAFE_INTEGER+3)
9007199254740994

所以写JS的同学们要注意,Number超过这个值后,可能会出现bug哦。

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/20040/

0%