先记住这个答案
Number 能表示的最小正数是 Number.MIN_VALUE,约等于 5e-324,精确值是 2 的 -1074 次方。运算结果如果比它的一半还小,就会下溢成 0。之所以有 +0 和 -0,是因为 IEEE 754 双精度浮点数用一个单独的比特存符号,数值部分全是 0 时,符号位是 0 就是 +0,是 1 就是 -0。两者用 === 比较相等,只有 Object.is 或 1 / x 这类运算能区分出来。
MIN_VALUE是最小正数- 比最小正数还小会下溢成 0,负数下溢成 -0
Object.is能区分正零和负零1 / -0得到负无穷大
最小正数和下溢
双精度浮点数有 11 位指数、52 位尾数。正常情况下尾数前面隐含一个 1,这种叫规格化数,最小是 2 的 -1022 次方,约 2.2e-308。为了让数值更平滑地逼近 0,标准还定义了非规格化数,去掉隐含的 1,最小可以到 2 的 -1074 次方,这就是 Number.MIN_VALUE。
console.log(Number.MIN_VALUE) // 最小正数
console.log(Number.MIN_VALUE === 2 ** -1074)
console.log(Number.MIN_VALUE / 2) // 再小一半就下溢成 0
console.log(2 ** -1022) // 最小的「规格化」正数
console.log(Number.MIN_SAFE_INTEGER) // 最小安全整数,是个负数
// 负数下溢,结果是负零
const tiny = -Number.MIN_VALUE / 2
console.log(tiny === 0, Object.is(tiny, -0))查看输出与解释
5e-324
true
0
2.2250738585072014e-308
-9007199254740991
true trueNumber.MIN_VALUE / 2 正好落在 0 和最小正数的正中间,按「就近舍入,平局取偶」的规则被舍成了 0,这就是下溢。下溢不会报错,也不会给出 NaN,只是悄悄变成 0。
最后两行是负零的来源之一:一个极小的负数下溢时,数值没了,但符号还在。这正是负零存在的意义,它保留了「这个结果是从负方向逼近 0 的」这个信息。
负零怎么产生、怎么识别
负零在日常代码里不常见,但一些普通运算会悄悄产生它:负数乘以 0、Math.round(-0.4)、负数下溢、-0 字面量。它和 0 的区别只在少数几个地方露出来。
=== 分不出来,除法和 Object.is 能JavaScriptconst nz = -0
console.log(nz === 0) // === 认为相等
console.log(Object.is(nz, 0)) // Object.is 能区分
console.log(1 / nz, 1 / 0) // 除法把符号暴露出来
console.log(String(nz), JSON.stringify(nz)) // 转字符串时符号丢了
// 这些运算会悄悄产生 -0
console.log(Object.is(Math.round(-0.4), -0))
console.log(Object.is(0 * -5, -0))
console.log(Math.atan2(0, -0), Math.atan2(0, 0))查看输出与解释
true
false
-Infinity Infinity
0 0
true
true
3.141592653589793 0=== 和 == 都把 +0 和 -0 当成相等,includes、Set、Map 用的 SameValueZero 也一样。只有 Object.is 用的 SameValue 会区分。在没有 Object.is 的年代,判断 -0 的写法是 x === 0 && 1 / x === -Infinity。
String(-0) 和 JSON.stringify(-0) 都得到 '0',所以把 -0 渲染到页面上是看不出来的。反过来 toLocaleString 在部分环境下会输出 '-0',展示层遇到过就知道原因了。
- 负零在业务里什么时候有用
- 符号本身有时是信息。比如一个表示速度或方向的值,从 -0.3 逐渐衰减到 0,保留负号就知道物体最后是朝反方向运动的。
Math.atan2(0, -0)得到 π 而不是 0,也是靠负零区分了方向。大多数业务代码不需要关心它,但在做数值格式化、动画插值或者写对比工具函数时要考虑进去。
容易答错的地方
- 把
Number.MIN_VALUE当成最小的数 - 它是最接近 0 的正数,不是最小值。比它小的数多的是,比如 0、所有负数、
-Infinity。要表示「最小的有限数」应该用-Number.MAX_VALUE,写循环找最小值时初始值用Infinity,别用Number.MIN_VALUE。 - 以为下溢会报错或变成
NaN - JavaScript 的数值运算基本不抛异常。上溢变成
Infinity,下溢变成 0,只有0 / 0、Infinity - Infinity这类没有意义的运算才得到NaN。 - 以为
Object.is和===只差在NaN - 它们有两处不同:
Object.is(NaN, NaN)是true,Object.is(0, -0)是false。只记住第一处,就解释不了为什么 React 的状态比较会把 0 和 -0 当成不同的值。
面试官还会怎么问?
Number.EPSILON 和 Number.MIN_VALUE 有什么区别?
Number.EPSILON 约 2.22e-16,是 1 和比 1 大的下一个浮点数之间的距离,用来描述 1 附近的精度,常用于浮点比较的容差。Number.MIN_VALUE 是整个数轴上离 0 最近的正数,描述的是能表示的最小量级,两者差了几百个数量级。
数组的 includes 和 indexOf 对 -0 和 NaN 怎么处理?
includes 用 SameValueZero,把 +0 和 -0 视为相等,也能找到 NaN。indexOf 用严格相等,同样把 +0 和 -0 视为相等,但找不到 NaN,[NaN].indexOf(NaN) 是 -1。
怎么把一个可能是 -0 的值规范成 0?
加 0 就行,-0 + 0 的结果是 +0。也可以写 x || 0,因为 -0 是假值,不过这个写法会把其它假值也变成 0,要确认输入一定是数字。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。