先记住这个答案
JavaScript 不区分整数和小数,所有 Number 都按 IEEE 754 双精度格式存成 64 位:最高 1 位是符号,接着 11 位是指数,剩下 52 位是尾数。一个数的值等于 ±1.尾数 × 2^(指数 - 1023),其中 1023 叫偏移量,用来让 11 位无符号数也能表示负指数。指数位决定数值的范围,最大约 1.8e308;尾数位决定精度,加上隐含的那个 1 一共 53 位二进制有效数字,换算成十进制大约 15~17 位。
- 64 位 = 1 符号 + 11 指数 + 52 尾数
- 指数存的是实际值加 1023 的偏移结果
- 尾数前面隐含一个 1,实际精度 53 位
- 指数管范围,尾数管精度
把一个数拆开看
直接看位最直观。DataView 可以把一个 Number 按 64 位浮点写进内存,再按整数读出来,就能拿到它的二进制。
function bits(x) {
const view = new DataView(new ArrayBuffer(8))
view.setFloat64(0, x) // 按双精度写入 8 个字节
const all = view.getUint32(0).toString(2).padStart(32, '0') +
view.getUint32(4).toString(2).padStart(32, '0')
const exp = parseInt(all.slice(1, 12), 2)
return `${x} 符号=${all[0]} 指数=${exp}(实际${exp - 1023}) 尾数前8位=${all.slice(12, 20)}`
}
console.log(bits(1))
console.log(bits(-2))
console.log(bits(6))
console.log(bits(0.1))查看输出与解释
1 符号=0 指数=1023(实际0) 尾数前8位=00000000
-2 符号=1 指数=1024(实际1) 尾数前8位=00000000
6 符号=0 指数=1025(实际2) 尾数前8位=10000000
0.1 符号=0 指数=1019(实际-4) 尾数前8位=10011001逐个对一下。1 写成二进制是 1.0 × 2^0,实际指数 0,加上偏移存成 1023,尾数全 0。-2 是 1.0 × 2^1,符号位变成 1。6 是二进制 110,也就是 1.1 × 2^2,小数点后的那个 1 进了尾数第一位。0.1 是 1.1001100110011… × 2^-4,尾数是 1001 无限循环,52 位存不下只能截断舍入,这就是 0.1 + 0.2 不等于 0.3 的根源。
你会发现尾数里从来不存小数点前面那个 1。规格化的二进制数第一位一定是 1,既然固定就不用浪费一位,这叫隐含位。所以 52 位尾数实际有 53 位精度。
指数管范围,尾数管精度
这两段各管一件事,面试时把它们分开讲最清楚。
- 指数决定能表示多大、多小
- 11 位指数能存 0~2047。0 和 2047 被保留,普通数字能用的是 1~2046,减掉 1023 后实际指数范围是 -1022 到 1023。尾数全 1 再配上最大指数,就是
Number.MAX_VALUE,约 1.7976931348623157e308,再乘 2 就变成Infinity。最小的规格化正数是 2^-1022,约 2.2e-308。 指数全 0 时规则变了:不再隐含那个 1,数值变成 0.尾数 × 2^-1022,这叫非规格化数,用来在 0 附近再往下挤出一段。最小的一个是 2^-1074,也就是Number.MIN_VALUE,约 5e-324。 - 尾数决定能精确到哪一位
- 53 位有效数字意味着,在 2^53 以内每个整数都能精确表示,这就是
Number.MAX_SAFE_INTEGER等于 2^53 - 1 的来源。超过 2^53 后,相邻两个能表示的数之间的间隔变成 2,2 ** 53 + 1算出来还是2 ** 53。数越大,间隔越大,到了 1e308 附近间隔已经是天文数字了。 所以「范围大」和「精度高」是两回事:Number 能表示 1e308 这么大的数,但只能记住它的前 15~17 位十进制有效数字,后面全是近似。 - 指数全 1 的两种特殊值
- 指数全 1、尾数全 0 表示
Infinity,符号位决定正负;指数全 1、尾数不为 0 就是NaN。尾数有 52 位,所以理论上有很多种不同位模式的NaN,这也是NaN不等于自身的一个直观理解:它不是一个具体的数,而是一类「不是数」的标记。
容易答错的地方
- 以为 JavaScript 有单独的整数类型
- Number 只有双精度浮点这一种存储,整数只是尾数后面恰好为 0 的浮点数。真正的任意精度整数是 BigInt,它是另一种原始类型,不能和 Number 直接混算。
- 把
Number.MIN_VALUE当成最小的负数 Number.MIN_VALUE是最接近 0 的正数,约 5e-324。最小的负数是-Number.MAX_VALUE。- 以为位运算也是按 64 位做的
&、|、<<这些位运算会先把操作数转成 32 位有符号整数再算,结果也是 32 位。2 ** 32 | 0得到 0,就是高位被丢掉了。拿位运算取整,只适用于 32 位范围内的数。
面试官还会怎么问?
为什么安全整数上限是 2^53 - 1 而不是 2^53?
2^53 本身能精确表示,但 2^53 + 1 不能,会被舍入成 2^53。「安全」的意思是这个整数和它的相邻整数都能被区分开,Number.isSafeInteger(2 ** 53) 返回 false,所以上限取 2^53 - 1。
后端返回的 64 位 ID 在前端变了,怎么回事?
比如 Java 的 long 型 ID 超过了 2^53,JSON.parse 把它转成 Number 时尾数存不下,末几位被舍入,ID 就变了。常见解法是让后端把这类字段序列化成字符串,前端不要当数字处理;需要运算时再用 BigInt 转换。
为什么会有 -0?
因为符号位是独立的一位,数值部分全 0 时符号位可以是 0 也可以是 1。比如一个很小的负数下溢,或者 -1 * 0,结果就是 -0。它和 0 用 === 比较相等,但 1 / -0 是 -Infinity,用 Object.is 能区分出来。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。