Number类型
Number 类型
JS 里的Number
类型值,没有 Int 整数,全都是浮点数,并且以 IEEE 二进制浮点算术标准中所指定的双精度 64 位格式来进行表示,也就是上文介绍的 64 位双精度浮点数。
Number 直接量形式
JS 里能表示数字的形式总体来说就是 4 种:
- 小数形式
- 十进制整数组合
[0,9]
+ 小数点.
+ 可选的小数位数 + 科学计数法表示:2.2e8 - 小数点
.
开头 +[0, 9]
组合 + 科学计数法表示:0.2e8 - 十进制整数组合
[0,9]
+ 科学计数法形式:2e8- 科学计数法表示:
e
或E
+ 正负号±
+[0, 9]
组合
- 科学计数法表示:
- 十进制整数组合
- 十进制 BigInt 形式
0n
- 数字[1, 9]开头 + 数字[0, 9]组合 +
n
- 非十进制整数形式
- 二进制整数
0b
或0B
+[0, 1]
组合 - 八进制整数
0o
或0O
+[0, 7]
组合 - 十六进制整数
0x
或0X
+[0, 9]
或[a, f]
或[A, F]
组合
- 二进制整数
- 非十进制整数 + BigInt 后缀
n
的形式
此外还有两个特殊的变量值,Infinity
和NaN
,它们既可以被看作全局对象window
或者globalThis
的属性,也可以看作一个特殊的Number
类型的值,或者作为Number
构造函数的静态属性。
Infinity
表示无穷,JS 里的最大数接近于2^1024 ≈ 1.79E+308
,当超过这个数值范围后,就用Infinity
表示,Infinity
可以带有正负号±
。
NaN
,也就是 not a number 的意思,被用来表示一些非法的计算操作的结果,例如0/0
这种明确规定除数不能为 0 的情况,为了不报错,就指定NaN
作为它们的返回结果。NaN
有两个特点:
- 任何涉及
NaN
的操作结果都是NaN
; NaN
不等于任何数,包括它自己
window.Infinity;
globalThis.Infinity;
Number.POSITIVE_INFINITY;
Number.NEGATIVE_INFINITY;
Number.NaN;
+0 和-0
- 关于
+0
和-0
,它们在数值上是一样的,用 64 位二进制表示只有符号位不一样的区别 0/0
结果会是NaN
;其他任何数除以0
都会得到+Infinity
;其他任何数除以-0
都会得到-Infinity
。- 忘记检测除以
-0
,而得到负无穷大的情况经常会导致错误,而区分+0
和-0
的方式,正是检测1/x
是Infinity
还是-Infinity
console.log(0 / 0); //NaN
console.log(1 / 0); //+Infitnity
console.log(1 / -0); //-Infitnity
Number 构造函数
Number
构造函数依旧拥有两种用途
- 当使用
new Number(value)
时,会创建一个Number
类型的对象,其[[prototype]]
指向Number.prototype
; - 而单独使用
Number(value)
时,相当于类型转换
执行步骤如下:
- 如果没有提供参数参数
value
,那么数字值就是+0
;如果提供参数value
,判断其类型,如果是Object
会先转换成原始值类型;获取原始值类型后,根据原始值类型转Number
类型的规则,获取数字值; - 判断是否是通过
new
调用的,如果是则根据Number.prototype
创建新的对象,将其私有属性[[NumberData]]
指向上一步获取的数字值; - 如果是直接调用的构造函数,那么就直接返回第一步获取的数字值
Number 静态属性
Number.NaN
也就是变量NaN
Number.MAX_VALUE
JS 中最大正数,约等于1.79E+308
,超过这个数用Infinity
表示。
按照 64 位二进制的形式确定 JS 最大正数:
- 首先符号位肯定是正的,也就是二进制
0
; - 其次指数部分,去掉指数部分 11 位全是
1
(也就是表示±Infinity
和NaN
)的情况,那只能是11111111110
的情况下指数最大,得到指数实际最大值为2^11-2-1023=1023
; - 有效数字部分 52 位加最前面的
1.
部分,得到 53 位二进制全是 1 的情况1.1111xxx
;转换成十进制也就是(2^53-1)*2^(-52)
Number.MIN_VALUE
JS 最小正数,约等于5e-324
。
同理,最小正数:
- 符号位指定二进制
0
; - 指数部分 11 位全是
0
,得到指数实际值是1-1023 = -1022
; - 然后有效数字部分最后一位是
1
,然后再在有效数字的基础上小数点向左移位 1022 个位数,也就是1
前面有 1073 个0
,把这个二进制数转换成十进制就是 JS 最小正数Number.MIN_VALUE
Number.POSITIVE_INFINITY
正无穷+Infinity
Number.NEGATIVE_INFINITY
负无穷`-Infinity``
Number.MAX_SAFE_INTEGER
最大安全整数Number.MAX_SAFE_INTEGER
,也就是2^53-1
,最小安全整数Number.MIN_SAFE_INTEGER
是 53 位最后一位置1
,也就是-(2^53-1)
;在这个范围内的整数都能保证在转换成二进制后再转回来和原来一样,超过了就会出现不连续的情况。
从 64 位二进制角度看,2^53-1
就是二进制科学计数法的尾数部分全是1
,向右移位 52 位,也就是指数是52+1023=1075
。如果这个数再加 1,就还需要再往右移一位然后其他位置0
才可以,这时候二进制科学计数法的尾数其实是1.00000
(后面 53 个 0),于是指数就变成了53+1023=1076
;你刚说位数需要 54 位?对不起,最多只给你 53 位,于是从 53 位开始丢掉,所以2^53
损失了一位精度,它不能算作安全的整数
2^53+1
是多少呢,2^53+1
也就是2^53-1+2
,同样需要 54 位有效数字,不过尾数最后面一位是1
,但是只给 53 位,于是丢掉丢掉,得到的是和2^53
一样的二进制数,也就是Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2
- 从 53 位开始,第一个进制被舍弃,每 2 个值都会有一个值出现这种不精确的情形,他们 64 位二进制形式都一样;再过 1 个值,会出现每 4 个值里面都有 3 个值不精确
Math.pow(2, 53) - 1; //9007199254740991
Math.pow(2, 53); //9007199254740992
Math.pow(2, 53) + 1; //9007199254740992
Math.pow(2, 53) + 2; //9007199254740994
Math.pow(2, 53) + 3; //9007199254740996
Math.pow(2, 53) + 4; //9007199254740996
Math.pow(2, 53) + 5; //9007199254740996
Number.MIN_SAFE_INTEGER
JS 最小安全整数-(2^53 - 1)
。
Number.EPSILON
Number.EPSILON
表示的是1
和最小的正数Number.MIN_VALUE
的差值,这个值接近于2^-52
;
Number.EPSILON
经常被用来表示 JS 里能接受的最小误差,当浮点数的运算结果和实力期望值之间的差值小于这个值时,就认为结果是正确的,例如
console.log(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
精度丢失的问题
64 位双精度浮点数表示法的最高精度是 17 位小数,这个结论是因为,对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001
,小数点后面有连续 51 个零,这个值减去 1 之后,就等于2^(-52)
,也就是 JS 最小的大于 1 的浮点数是2^(-52)
,这个值接近于 2.2E-16
,而科学计数法的精度以尾数的最后一个数在原数中的位数为标准,所以就是 2 在0.00000000000000022
中的数位,也就是小数点后 17 位。
计算机中的数只有 0 和 1 二进制形式,任何形式的运算,例如加减乘除等都是将数字转换成二进制表示然后在二进制的基础上计算的,而 JS 中表示数值的二进制位数只有 52 位,不管加减乘除肯定会涉及到舍入进位的问题,当舍入进位后的位数超过 52 位时,就会丢失精度。
0.1+0.2≠0.3
有一个网站叫https://0.30000000000000004.com/
,可以查看各种语言在0.1+0.2
这个表达式的结果。
关于0.1+0.2
,把他们转成 64 位二进制浮点数的形式进行二进制的加法运算以后会存在精度丢失的问题,关于二进制的浮点计算,可以参考这篇文章关于 js 中的浮点计算
0.1D = 2^-4 * 1.1001100110011001100110011001100110011001100110011010B
0.2D = 2^-3 * 1.1001100110011001100110011001100110011001100110011010B
0.3D = 2^-2 * 1.0011001100110011001100110011001100110011001100110011B
//0.1 + 0.2 时,先将两者指数统一为 -3,也就是让小数点在64位二进制里的位置相同,故 0.1 小数点向左移一位
0.1100110011001100110011001100110011001100110011001101B
+ 1.1001100110011001100110011001100110011001100110011010B
------------------------------------------------------------
= 10.0110011001100110011001100110011001100110011001100111B
//小数点往左移一位使得整数部分为 1,此时尾数部分为 53 位,进一舍零,于是得到最后的值是
2^-2 * 1.0011001100110011001100110011001100110011001100110100
也就是 0.010011001100110011001100110011001100110011001100110100
转成十进制就是 0.30000000000000004 15个0
//所以
console.log(0.1 + 0.2 === 0.3); //false
正确比较浮点数的方法
JS 正确的比较 JS 浮点数运算结果的方式是,利用绝对值和最小精度来做比较
console.log(Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
精度取舍的问题
num.toFixed(n)
,0≤n≤20,默认为 0
- 准确的说,使用
toFixed(n)
是一种定点计数法,定点数就是小数点后位数固定的实数,与之相对的是浮点数,toFixed(n)
是将数字的 64 位二进制形式转换成定点数,然后再转换成字符串,所以不会单纯的进行四舍五入的运算 - 如果
precision
参数不在 1 和 20(包括)之间,将会抛出一个RangeError
; toFixed(n)
会在转换的时候进行四舍五入的运算,遇到不同小数位数的5
舍入的结果也会不同,这个原因的具体细节可以参考这篇文章——为什么(2.55).toFixed(1)等于 2.5?
(1.005).toFixed(2); //1.00
(1.55).toFixed(1); //1.6
(2.55).toFixed(1); //2.5
(3.55).toFixed(1); //3.5
numObj.toPrecision(precision)
,0≤precision≤100
- 以指定的精度返回该数值对象的字符串表示,也就是说参数
precision
是有效数字的个数 toPrecision
也会进行四舍五入,如果忽略precision
参数,这个方法就相当于toString()
;如果precision
不是整数,会对precision
进行向下取舍,也就是取最接近precision
但是不大于precision
的整数;如果precision
参数不在 1 和 100 (包括)之间,将会抛出一个RangeError
toPrecision
可能会返回科学计数法的形式
Math.round(x)
- 返回一个数字四舍五入后最接近的整数
- 要深刻理解最接近是什么意思,画个数轴就明白了,有这么几种情况
- 正数且小数部分大于
0.5
,向上取整,也就是最接近的整数 - 正数且小数部分等于
0.5
,还是向上取整 - 负数且小数部分大于
0.5
,那肯定往左最接近 - 负数且小数部分小于
0.5
,那肯定往右取整去接近
Math.round(20.49); //20
Math.round(20.5); //21
Math.round(20.51); //21
Math.round(-20.49); //-20
Math.round(-20.5); //-20
Math.round(-20.51); //-21
建议借助第三方库——mathjs