JS数字之旅——Number
首先来一段神奇的数字比较的代码
23333333333333333 === 23333333333333332
// output: true
233333333333333330000000000 === 233333333333333339999999999
// output: true
咦?明明不一样的两个数字,为啥是相等的呢?
Number
众所周知,每一种编程语言,都有自己的数字类型,像Java里面,有int
、float
、long
、double
等,不同的类型有不同的可表示的数字范围。
同理,JavaScript也有Number
表示数字,但是没有像C、Java等语言那样有表示不同精度的类型,Number
可以用来表示整数也可以表示浮点数。由于Number
是在内部被表示为64位的浮点数,所以是有边界值,而这个边界值如下:
Number.MAX_VALUE
// output: 1.7976931348623157e+308
Number.MIN_VALUE
// output: 5e-324
最大正数和最小正数
Number.MAX_VALUE
代表的是可表示的最大正数,Number.MIN_VALUE
代表的是可表示的最小正数。它们的值分别大约是1.79E+308
和5e-324
这时很容易想到一个问题,那超过MAX_VALUE
会发生什么呢?通过下面的代码和输出可以发现,当超过MAX_VALUE
,无论什么数字,都一律认为与MAX_VALUE
相等,直到超过一定值之后,就会等于Infinity
。
Number.MAX_VALUE
// output: 1.7976931348623157e+308
Number.MAX_VALUE+1
// output: 1.7976931348623157e+308
Number.MAX_VALUE+1e291
// output: 1.7976931348623157e+308
Number.MAX_VALUE === Number.MAX_VALUE+1e291
// output: true
Number.MAX_VALUE+1e292
// output: Infinity
很明显,最开始的那段代码里面的数字,是在这两个Number.MAX_VALUE
和Number.MIN_VALUE
,没有出现越界的情况,但为什么会发生比较上错误呢?
安全整数
其实,Number
还有另一个概念,叫做安全整数,其中MAX_SAFE_INTEGER
的定义是最大的整数n,使得n和n+1都能准确表示。而MIN_SAFE_INTEGER
的定义则是最小的整数n,使得n和n-1都能准确表示。如下面代码所示,9007199254740991(2^53 - 1
) 和 -9007199254740991(-(2^53 - 1)
) 就是符合定义的最大和最小整数。
Number.MAX_SAFE_INTEGER
// output: 9007199254740991
Number.MAX_SAFE_INTEGER+1
// output: 9007199254740992
Number.MAX_SAFE_INTEGER+2
// output: 9007199254740992
Number.MIN_SAFE_INTEGER
// output: -9007199254740991
Number.MIN_SAFE_INTEGER-1
// output: -9007199254740992
Number.MIN_SAFE_INTEGER-2
// output: -9007199254740992
这意味着,在这个范围内的整数,进行计算或比较都是精确的。而超过这个区域的整数,则是不安全、有较大误差的。
现在回头看最开始的代码,很明显这些数字都已经超过MAX_SAFE_INTEGER
。另外,也可以使用Number.isSafeInteger
方法去判断是否是安全整数
Number.isSafeInteger(23333333333333333)
// output: false
Infinity
关于Infinity
的一个有趣的地方是,它是一个数字类型,但既不是NaN,也不是整数。
typeof Infinity
// output: "number"
Number.isNaN(Infinity)
// output: false
Number.isInteger(Infinity)
// output: false
进制
JavaScript里面,除了支持十进制以外,也支持二进制、八进制和十六进制的字面量。
- 二进制:以
0b
开头 - 八进制:以
0
开头 - 十六进制:以
0x
开头
0b11001
// output: 25
0234
// output: 156
0x1A2B
// output: 6699
里面出现的字母是不区分大小写,可以放心使用。那当我们遇到上面的进制以字符串形式出现的时候,如何解决呢?答案是使用parseInt
parseInt('11001', 2)
// output: 25
parseInt('0234', 8)
// output: 156
parseInt('0x1A2B', 16)
// output: 6699
parseInt
这个方法实际上支持2-36进制的转换,虽然平时绝大多数情况,它通常被用来转十进制格式的字符串,而不会特别声明第二个参数。需要注意的一个特别的点是,部分浏览器,如果字符串是0开头的话,不带第二个参数的话,会默认以八进制进行换算,从而导致一些意想不到的bug。所以,保险起见,还是应该声明第二个参数。
浮点数计算
前面基本上都是在讨论整数,Number
在浮点数计算方面,就显得有些力不从心,经常出现摸不着头脑的情况,最著名的莫过于0.1 + 0.2
不等于0.3
的精度问题。
0.1 + 0.2 === 0.3
// output: false
0.1 + 0.2
// output: 0.30000000000000004
1.5 * 1.2
// output: 1.7999999999999998
但其实这并非JavaScript特有的现象,像Java等语言也是有这种问题存在。究其原因,是因为我们以十进制的角度去计算,但计算机本身是以二进制运行和计算的,这就会导致在浮点数类型的表示和计算上,会存在一定的偏差,当这种偏差累计足够大的时候,就会导致精度问题。
正所谓解决办法总比困难多,仔细想想还是能找到一些解决方案。
有一种思路是,既然尽管出现误差也只有一点点,那就通过toFixed()
强行限制小数位数,让存在误差的数值进行转换从而消除误差,但有一定的局限性,当遇上乘法和除法的时候,需要确认限制小数位数具体要多少个才合适。
另一种思路是,浮点数计算有误差,但是整数计算是准确的,那就把浮点数放大转换为整数,然后进行计算后再转换为浮点数。跟上面的方案类似,同样需要解决放大多少倍,以及后面转换为浮点数时的额外计算。
链接:https://juejin.cn/post/7001183062792863774