每日一报
js的最大安全数字为什么是
javascript
中的所有数字都是用 64
位的 IEEE 754
双精度浮点数表示的。
IEEE 754 双精度浮点数
双精度浮点数由三个部分组成:
其中:
- sign(符号位):
1
位,用于表示数值的正负。0
表示正数,1
表示负数。 - exponent(指数部分):
11
位,用于表示数值的数量级。它决定了浮点数的规模,通过对11
位的二进制数进行偏移表示(即对指数加上一个偏移量,称为bias
)。 - fraction(尾数部分):
52
位,有效位数,也称为尾数,表示数值的精度。
指数部分的具体作用
指数部分是一个 11
位的无符号整数,表示的范围是 0(bias
)的方式来表示。对于双精度浮点数,bias
是 1023
。
样例说明
实际指数值 = 存储的指数值 - 偏移量(1023
)
- 如果指数部分存储的是
0
(即二进制的00000000000
),则实际指数是0 - 1023 = -1023
,此时通常用于表示特殊值,如次正规数或零。 - 如果指数部分存储的是
1023
(即二进制的01111111111
),则实际指数是1023 - 1023 = 0
,表示的是,即数字的值不需要放大或缩小。 - 如果指数部分存储的是
1024
,则实际指数是1024 - 1023 = 1
,表示的是,即数字的值需要乘以 2
。 - 如果指数部分存储的是
1022
,则实际指数是1022 - 1023 = -1
,表示的是,即数字的值要缩小两倍。 - 如果指数部分存储的是
2047
(即二进制的11111111111
),则实际指数是2047 - 1023 = 1024
,此时通常通常用于表示特殊值,如无穷大
或NaN
。
11
位无符号整数可以表示的范围是 0
到 2047
,指数位 0
和指数位存储 2047
时,即有效区间 [1, 2046]
),则可以表示的范围是
可以看出双精度浮点数在 JavaScript 中可以表示非常大的数值范围,但是在超过安全整数范围时,可能会出现精度丢失的原因。
那么问题来了,最大的安全整数为什么为 Math.pow(2,53) - 1
呢?
由公式
可以得知 fraction(尾数部分)
最大为 52
位数,再加上前面 1
(由于 IEEE 754 标准在尾数部分隐含了一个 1
(规范化数的前导 1
),因此尾数实际上具有 53
位的精度。)。指数位(1023
位),那么理论上双精度浮点数可以表示的最大值为
这里需要注意一点,双精度浮点数整数的安全精度是由尾数位(
1.0000000000000000000000000000000000000000000000000000
通过指数位右移 52
位:
10000000000000000000000000000000000000000000000000000
可知最大的安全位数为 53
位。
那么最大可以表示的精确整数范围是从 Number.MIN_SAFE_INTEGER
) 到 Number.MAX_SAFE_INTEGER
)。
那么最大可以表示的精确整数范围是
Number.isSafeInteger(2 ** 53 - 1); // true
Number.isSafeInteger(2 ** 53); // false
Number.isSafeInteger(-(2 ** 53) + 1); // true
Number.isSafeInteger(-(2 ** 53)); // false
当整数超过 53
位的精度,相邻整数间的差值可能无法在浮点表示中区分开。例如:
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true
与位运算的区别
javascript
中的位运算是一个特殊的操作集合,与普通算术运算有本质区别。在执行位运算时,javascript
会将操作数转换为 32
位有符号整数(而非 64
位浮点数),因此位运算的安全数值范围为 [-
这是与 javascript
常规运算的第一个关键区别:
// 常规算术运算使用64位浮点数
console.log(9007199254740991 + 1); // 9007199254740992
// 位运算会先转换为32位整数
console.log(9007199254740991 | 0); // -1 (截断到32位)
位运算时的转换遵循以下规则:
- 将操作数转换为整数,忽略小数部分。
- 将整数截断为
32
位。 - 执行位运算。
- 结果仍保持为
32
位整数。
// 小数部分被截断
console.log(5.7 & 3.2); // 1 (5 & 3 = 1)
// 大于32位的整数被截断
console.log((2 ** 32 + 1) << 1); // 2 (而非 2 ** 33 + 2)
根据 ECMAScript
规范,位运算符会对操作数应用 ToInt32
抽象操作。这意味着:
- 非数值会被转换为数值(类似
Number()
)。 - 数值会被转换为
32
位有符号整数。 NaN
和Infinity
会被转换为0
。- 小数部分被直接丢弃(向
0
取整)。
// 非数值的转换
console.log('5' & 3); // 1 (字符串"5"转换为数字5)
console.log(true << 2); // 4 (true转换为1,左移2位)
// NaN和Infinity的处理
console.log(NaN >>> 1); // 0
console.log(Infinity & 1); // 0
特性 | 一般运算 | 位运算 |
---|---|---|
数值表示 | 64位双精度浮点数 | 32位有符号整数 |
精度范围 | ±(2^53-1) | ±(2^31-1) |
小数支持 | 支持 | 不支持(自动截断) |
NaN/Infinity处理 | 支持 | 转换为0 |
位运算的性能特性
位运算在现代 JavaScript
引擎中并不总是比算术运算更快。V8
等引擎的优化已使算术运算非常高效。但位运算在以下情况依然有优势:
// 取整操作
const x = 25.9;
// 位运算方式
console.log(x | 0); // 25 (Math.floor(x))
// 判断奇偶
console.log(7 & 1); // 1 (奇数)
console.log(8 & 1); // 0 (偶数)
// 乘除法替代
console.log(8 << 1); // 16 (8 * 2)
console.log(8 >> 1); // 4 (8 / 2)