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 -> 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做个试验,验证下:

> 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哦。

原文链接: https://pengrl.com/p/20040/

原文出处: yoko blog ( https://pengrl.com )

原文作者: yoko ( https://github.com/q191201771 )

版权声明:本文欢迎任何形式转载,转载时完整保留本声明信息(包含原文链接、原文出处、原文作者、版权声明)即可。本文后续所有修改都会第一时间在原始地址更新。