先记住这个答案
IEEE 754 浮点格式有独立的符号位,即使指数和尾数全部为零,符号位仍然可以是 1,所以存在两个零:+0 和 -0。JavaScript 的 === 特意把它们视为相等,-0 === 0 返回 true。要区分两个零,需要用 Object.is(-0, 0),它返回 false。另外 1 / -0 得到 -Infinity 而 1 / 0 得到 Infinity,通过倒数的符号也能观察到差异。
Object.is(-0, 0)为false1 / -0得到-InfinityString(-0)输出"0",符号丢失- Map 和 Set 把
+0和-0当同一个键
怎么产生负零
负零不需要显式写 -0,很多运算会隐式产生它。负数向零舍入、负数乘以零、负数除以 Infinity 都可能得到 -0。
var neg = -0;
console.log(neg === 0);
console.log(Object.is(neg, -0));
console.log(Object.is(neg, 0));
console.log(1 / neg);
console.log(String(neg));
console.log(JSON.stringify(neg));查看输出与解释
true
true
false
-Infinity
0
0第一行说明 === 分不出两个零。Object.is 能区分:Object.is(neg, -0) 是 true,Object.is(neg, 0) 是 false。1 / -0 得到 -Infinity,这也是一种间接检测方式。
最后两行是关键:String(-0) 和 JSON.stringify(-0) 都输出 "0",符号信息在转成文本时丢了。如果你把一个包含 -0 的对象序列化成 JSON 再反序列化,拿回来的就是普通的 0,负零的语义消失了。
Object.is 和其他相等算法怎么处理两个零
JavaScript 有四种相等算法,对 -0 的处理各不相同。=== 和 == 都把两个零当同一个值。Object.is 使用 SameValue 算法,能区分它们。includes、Map、Set 使用的 SameValueZero 算法和 === 一样,不区分两个零。
这意味着如果你用 -0 做 Map 的键,后面用 0 也能命中同一个键。Set 里加了 -0 之后再加 0,集合大小不会增加。在绝大多数业务场景下,这个行为是合理的——你不会希望 0 和 -0 被当成两个不同的值存两份。
但在数值算法里有时候需要保留这个信息。比如某个物理模拟计算速度,-0 表示从负方向趋近静止,+0 表示从正方向趋近静止。如果中间过程把 -0 丢了,后续计算方向就错了。遇到这种场景,在存储和传输时就不能用 JSON 或 String() 转换,需要自己约定编码方式。
容易答错的地方
- 用
value === -0检测负零 0 === -0是true,所以value === -0对正零也会通过,根本区分不了。正确的检测方式是Object.is(value, -0)。- 以为 JSON 往返能保留负零
JSON.stringify(-0)输出"0",JSON.parse("0")得到0。整个过程中-0的符号信息不可逆地丢失了。如果接口需要传递带符号的零,得在协议层另外约定。
面试官还会怎么问?
Math.sign(-0) 返回什么?
返回 -0,不是 -1。Math.sign 对零保留了原始符号。如果你想用 Math.sign 来判断正负,拿到 -0 时还是没法直接当 -1 用,需要额外处理。
怎么把显示中的 -0 变成 0?
在格式化输出前用 Object.is(value, -0) ? 0 : value 做一次归一化。只在确认业务不需要符号信息时才做,否则会掩盖有意义的数据。
为什么需要两个零?
IEEE 754 的设计初衷是让浮点运算在趋近零时保留方向信息。比如一个很小的负数除以一个很大的正数,数学上趋近于零但从负侧逼近,用 -0 可以记录这个方向。1 / -0 得到 -Infinity 也符合极限的概念:从负侧趋近零的倒数应该是负无穷。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。