
之前有前端同学问我,JavaScript中最大的数有多大。
那时就想写一些文章,从整型各种进制的转换,到原码反码补码的形式,最后到浮点数,再加上字符串类型的数字,把计算机世界里与数字相关的内容都说一说。
但由于对相关知识的掌握程度,表达能力,执行力等各方面原因,一直没有动笔。。
今天看到draveness大佬写了一篇 《为什么 0.1 + 0.2 = 0.300000004 · Why’s THE Design?》,很好的讲解了浮点数的知识。
所以本文就直接在大佬文章的基础之上讲解就好了。看本文前可以先看看大佬的文章。
正文
JS中Number类型的数字,不管是整数还是小数,底层都使用64位的浮点数形式存储。所以JavaScript中最大的数有多大,等价于64位浮点数最大的数有多大。
浮点数要解决的问题
我们先跳出实现细节,来谈谈为什么浮点数存在精度问题。
比如 0~100 这个范围内,整数的个数是有限的,就是101个。
而如果是小数,由于小数点之后的部分是无限的,比如我随便说两个小数, 1.2304 和 1.2300004 ,中间到底出现多少个0都是合法的小数,所以理论上是没办法使用有限的存储空间(比如64位)表示完所有的小数。
你可能很容易想到,限制小数点后的位数,比如最多两位,也即范围是 0.00~0.99 ,那么 0~100 范围内的数就变回有限了,也即 101*100=10100 个。
这种方式适用一些场景,比如人民币,如果单位是元,那么小数只需要两位,分别是角和分。
可惜的是,并不是所有场景,小数点后保留两位就够用,关于这点相信也不用我过多举例,拿数字 3.1415 来说,只能存储为 3.14 或 3.15 ,也即精度丢失了。
并且,如果总是预留一部分空间存储两位小数,那么也是一种浪费。
抽象来看,我们面临的问题实际上是,如何用有限的空间存储尽量大的数字范围,以及尽量高的精度。
某种角度,浮点数是一种解决上述问题的编码方式。
浮点数的原理
JS和大多数编程语言一样,采用 IEEE 754 浮点数标准。
在draveness的 文章 中,图文并茂的对该标准进行了描述,并分别举了 0.1 , 0.2 , 0.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) * 12 -> 1 * power(2, 1) * 13 -> 1 * power(2, 1) * (1 + 1/power(2, 1))4 -> 1 * power(2, 2) * 15 -> 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) * 11 -> 0 01111111 00000000000000000000000二进制01111111 = 十进制127,平移后得到exp = 0fraction = 07 -> 0 10000001 11000000000000000000000二进制10000001 = 十进制129,平移后得到exp = 2fraction前两位有值,所以是1/power(2, 1) + 1/power(2, 2)在 这个非常棒的网站 中,你可以输入任意数字,查看对应的32位浮点数是如何表示的。
浮点数的范围
回到 JavaScript中最大的数有多大 这个问题,这其实包含两个问题:
- JavaScript Number类型中,最大的那个正整数是多少(也即超过这个数就没法表示了)
- 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 )