先记住这个答案
Object.is(a, b) 和 a === b 都不做类型转换,结果在绝大多数情况下一样,只有两处不同。第一,NaN === NaN 为 false,Object.is(NaN, NaN) 为 true。第二,0 === -0 为 true,Object.is(0, -0) 为 false。所以当你需要识别 -0,或者需要一个满足「值等于自身」的比较(比如判断状态有没有变化)时,才用 Object.is。另外还有第三种相等 SameValueZero,被 includes、Set、Map 使用,它在 NaN 上像 Object.is,在 -0 上像 ===。
Object.is认为NaN等于NaNObject.is认为0和-0不相等- SameValueZero 认为
NaN与自身相等 - React 使用
Object.is比较依赖值
把差异摆在一起看
用几组值分别跑一遍 === 和 Object.is,再顺便看看数组和 Set 用的是哪种规则。
const pairs = [[NaN, NaN], [0, -0], [1, 1], ['1', 1]]
for (const [a, b] of pairs) {
const show = (v) => (Object.is(v, -0) ? '-0' : typeof v === 'string' ? JSON.stringify(v) : String(v))
console.log(show(a), 'vs', show(b), '| ===', a === b, '| Object.is', Object.is(a, b))
}
console.log([NaN].indexOf(NaN), [NaN].includes(NaN)) // -1 true
console.log(new Set([0, -0, NaN, NaN]).size) // 2查看输出与解释
NaN vs NaN | === false | Object.is true
0 vs -0 | === true | Object.is false
1 vs 1 | === true | Object.is true
"1" vs 1 | === false | Object.is false
-1 true
2代码里要自己写 show 函数,是因为 String(-0) 会输出 '0',直接打印看不出符号。这也说明 -0 平时很难被察觉。
后两行最实用。indexOf 内部用 ===,所以永远找不到 NaN;includes 用 SameValueZero,能找到。Set 同样用 SameValueZero,0 和 -0 被当成同一个元素,两个 NaN 也被合并,最后只剩两个成员。
-0 从哪来,为什么要区分
JavaScript 的数字是 IEEE 754 双精度浮点数,符号单独占一位,所以 0 有正负两种。大部分运算里两者表现一样,但有些地方符号会暴露出来。
-0 露出马脚的地方JavaScriptconst x = -0
console.log(x === 0, String(x)) // 看起来就是 0
console.log(1 / x) // -Infinity,符号还在
console.log(Math.round(-0.4), Object.is(Math.round(-0.4), -0))
console.log(JSON.stringify(-0), Object.is(-0 + 0, 0))查看输出与解释
true 0
-Infinity
0 true
0 trueMath.round(-0.4) 的结果是 -0,负数乘以 0、除以负无穷也会得到它。打印和 JSON.stringify 都把它显示成 0,但拿它做除数就会得到 -Infinity。在做方向相关的计算时,比如拖拽速度、坐标系里的朝向,符号有意义,这时就要用 Object.is(v, -0) 来判断。在 Object.is 出现之前,只能用 1 / v === -Infinity 这种绕弯的写法。
- React 为什么用
Object.is - React 判断
setState传入的新值和旧值是否相同、useEffect的依赖有没有变化,用的都是Object.is。原因在于NaN:如果用===,一个值为NaN的 state 每次比较都「不相等」,会导致不必要的重新渲染,甚至死循环。Object.is保证了同一个值一定等于自己。
容易答错的地方
- 以为
Object.is能比较对象内容 Object.is({}, {})是false。它和===一样只比较引用,不做深比较。React 里直接修改对象再setState不会触发更新,就是因为引用没变,Object.is返回了true。- 以为
includes和Object.is规则一样 [-0].includes(0)是true,Object.is(-0, 0)是false。includes用的是 SameValueZero,只在NaN上和Object.is一致,在±0上和===一致。- 判断
NaN只会用Object.is - 判断某个值是不是
NaN,最直接的写法是Number.isNaN(v)。全局的isNaN会先把参数转成数字,isNaN('abc')也是true,不能用来做严格判断。
面试官还会怎么问?
怎么手写 Object.is 的 polyfill?
分两步处理特殊情况。如果 a === b,还要排除 0 和 -0,写成 a !== 0 || 1 / a === 1 / b;如果 a !== b,只有两边都是 NaN 时才返回 true,写成 a !== a && b !== b,利用的正是 NaN 不等于自身。
Map 的 key 用 -0 会怎样?
Map 用 SameValueZero 比较 key,而且在存入时会把 -0 规范成 +0。所以 map.set(-0, 'a') 之后,map.get(0) 能取到 'a',遍历 key 时看到的也是 +0。
什么场景下应该默认用 Object.is 代替 ===?
一般不需要。业务代码里绝大多数比较不会碰到 NaN 和 -0,=== 更短、团队也更熟悉。需要写通用的「值是否改变」逻辑时,比如自己实现一个状态库、缓存或 memo 工具,才推荐用 Object.is,行为和 React 保持一致。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。