跳到主要内容

Number类型

Number 类型

The Number Type

JS 里的Number类型值,没有 Int 整数,全都是浮点数,并且以 IEEE 二进制浮点算术标准中所指定的双精度 64 位格式来进行表示,也就是上文介绍的 64 位双精度浮点数。

Number 直接量形式

Numeric Literals

JS 里能表示数字的形式总体来说就是 4 种:

  • 小数形式
    • 十进制整数组合[0,9] + 小数点. + 可选的小数位数 + 科学计数法表示:2.2e8
    • 小数点. 开头 + [0, 9]组合 + 科学计数法表示:0.2e8
    • 十进制整数组合[0,9] + 科学计数法形式:2e8
      • 科学计数法表示:eE + 正负号± + [0, 9]组合
  • 十进制 BigInt 形式
    • 0n
    • 数字[1, 9]开头 + 数字[0, 9]组合 + n
  • 非十进制整数形式
    • 二进制整数0b0B + [0, 1]组合
    • 八进制整数0o0O + [0, 7]组合
    • 十六进制整数0x0X + [0, 9][a, f][A, F]组合
  • 非十进制整数 + BigInt 后缀n的形式

此外还有两个特殊的变量值,InfinityNaN,它们既可以被看作全局对象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/xInfinity 还是 -Infinity
console.log(0 / 0); //NaN

console.log(1 / 0); //+Infitnity

console.log(1 / -0); //-Infitnity

Number 构造函数

The Number Constructor

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(也就是表示±InfinityNaN)的情况,那只能是11111111110的情况下指数最大,得到指数实际最大值为2^11-2-1023=1023
  • 有效数字部分 52 位加最前面的1.部分,得到 53 位二进制全是 1 的情况1.1111xxx;转换成十进制也就是(2^53-1)*2^(-52)

bVbSdk

image-20200630111722074

Number.MIN_VALUE

JS 最小正数,约等于5e-324

同理,最小正数:

  • 符号位指定二进制0
  • 指数部分 11 位全是0,得到指数实际值是1-1023 = -1022
  • 然后有效数字部分最后一位是1,然后再在有效数字的基础上小数点向左移位 1022 个位数,也就是1前面有 1073 个0,把这个二进制数转换成十进制就是 JS 最小正数Number.MIN_VALUE

bVbSdh

image-20200630112552524

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损失了一位精度,它不能算作安全的整数

bVbSdl

  • 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 位时,就会丢失精度。

image-20200630164015549

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 150

//所以
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)

  • 返回一个数字四舍五入后最接近的整数
  • 要深刻理解最接近是什么意思,画个数轴就明白了,有这么几种情况
  1. 正数且小数部分大于0.5,向上取整,也就是最接近的整数
  2. 正数且小数部分等于0.5,还是向上取整
  3. 负数且小数部分大于0.5,那肯定往左最接近
  4. 负数且小数部分小于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