这里暂且不提BigInt。 因为,我也只知道它能表示任意大的整数,不可与Number类型混用。实际上,是不是任意大,就不知道了。 不过比Number类型的最大值还大的数是可以表示的。
BigInt(Math.ceil(Number.MAX_VALUE) +3)
言归正传,Number类型不区分小数和整数,也就是其他语言中的整型和单双精度。
基本概念
专业术语,Number类型是64位双精度浮点型,遵循 IEEE754 标准, 通过 64 位来表示一个数字,(1 + 11 + 52),(符号位 + 指数位 + 小数部分有效位)
最大安全数字
MAX_SAFE_INTEGER
,Math.pow(2, 53) - 1 (9007199254740991),16 位十进制。
最大值
Number.MAX_VALUE
, 1.7976931348623157e+308,换算为10进制大概是179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858365。
310位十进制。
最大值很容易理解,超出就不能用Number类型表示了。那么最大安全值是什么鬼?这里的安全指的是精度。一个整数的精度,想当然就是1,小数的精度取决于小数位。
我们不妨尝试一下,
当超出最大安全值后,结果连1这个精度都保证不了。
0.1 + 0.2 =?
再来尝试一下小数的精度 ,也是经典的面试题,0.2+ 0.3 都是0.1的精度 ,没问题,还是得出动经典的0.1 + 0.2,问题来了,结果有17位小数。
js是如何存储数字的
上面,已经提到64位浮点型。
符号位
- 符号位决定了一个数的正负. 第一位就是符号位。
0表示正数,1表示负数。 记着就行。 可以强行理解为,默认符号位就是正,就是不需要,无 0,负的才需要特别说明1 。
指数位
- 接下来11位是指数位,也就是2的多少次幂。
指数部分决定了数值的大小,小数部分决定了数值的精度。指数部分一共有11个二进制位,因此区间长度就是2047。 然后这里,需要分一半给负数,指数小于零,就是小数。
这个区间就是[-1023, 1024]。 因此最大值就是
Math.pow(2, 1024) // Infinity
其实这个数字还是挺大的, 13.4亿也不过才10位数(当然某人可能要从元谋人时期到现在还不一定能挣到),一般来说足够用了。
小数位
- 最后52位是小数位, 即有效数字。
注意,IEEE 754 规定,如果指数部分的值在0到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,当指数位大于零, 要表示的数字不小于1时,有效数字总是1.xx...xx
的形式,其中xx..xx
的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript 提供的有效数字最长为53个二进制位。
最大安全值为 2^53-1.
例如 0 000 000 000 10 1100000000000000000000000000000000000000000000000000 符号位 0 表示正 指数位10 即2 小数位11 即3
结果就是 1.11 X 10^10 = 1.75 X 2^2 = 7
同时这也说明了实际上,js没有整数,全部都是浮点数,小数。
浮点数
那么,扩展一下,什么是浮点数。
所谓浮点数就是小数点在逻辑上是不固定的。因为计算机里的存储都是二进制,01。 小数部分同样是用二进制的小数来表示的,比如0.5 就是二分之一,很容易表示为二进制的0.1,0.25就是0.01。那么问题来了, 0.1如何用二进制来表示呢,用我们常规办法,辗转相除,不断的用2作为除数。如果有余数,就用余数除以2,直到除尽。除不尽就是无限小数了。
因此0.1 = 1/16 + 1/32 + 1/256 + 1/512 + ......。 借助浏览器我们可以输入 0.1.toString(2)。结果如下 0.0001100110011001100110011001100110011001100110011001101。好像是一个无限循环小数,length为57,除掉前面的零刚好52位。浮点数定义也说了,浮点数属于有理数,即无限循环小数。
有心人可能计算了一下,0.1 + 0.2 = '0.0100110011001100....'= 1/4 + 1/32 + 1/64 + 1/512
可以停了。
结果似乎是小于0.3 的。那为什么打印出来大于0.3 ,当然是类似十进制的四舍五入了,浮点数的舍入。这个就不扩展了,跑题了。
3/10 - 1/4 = 1/20
1/20 - 3/64 = 1/320
1/320 - 1/512 ...我已经不想算了
最后
最大安全值,确实有可能会超。 我就碰到过,直接让后端转字符串了,因为用途拿来作ID唯一键的。不需要参数计算,只需要不失真。具体情况具体分析 , 如果还需要计算,可以用BigInt 也可以发请求扔给后端计算,也可以分段计算,自己实现对应的计算逻辑,最好的逻辑可能是别人的工具库。
然后就是小数丢失精度,只有一个办法,那就是不让小数参与运算.计算结果可以有小数,但不能让浮点数参与运算。无非就是先用十进制的左移,让它变成整数,计算结束后,再右移回去。这个方法不适用不能整除的除法,因为,不能让小数参与运算。
然后就是经典问题 0.1 + 0.3 的结果是什么?
答: 结果是js精度的0.3。 0.1+ 0.2 - 0.3 < Number.EPSILON // true
如果对方还要问,你就给他展开讲讲。
Number.EPSILON
属性表示 1 与Number
可表示的大于 1 的最小的浮点数之间的差值。 这句话的意思就是 Number.ESPLION
是js数字的精度。 就像我们说的精确到小数点后几位。