Object.is 能正确识别 NaN 相等,因其采用 SameValue 算法,使 Object.is(NaN, NaN) 为 true,且区分 +0 和 -0;它不进行类型转换,比 === 更严格,适用于精确值比较场景。
NaN === NaN 返回 false,这是 JavaScript 的语言规范决定的——任何值与 NaN 用严格相等比较都为 false。这导致用 Set 或 Array.prototype.filter 去重时,NaN 被当作不同值反复保留。
Object.is 是唯一把 NaN 当作“自身相等”的内置方法:Object.is(NaN, NaN) 返回 true,且它还修复了 0 和 -0 的误判问题(+0 === -0 为 true,但 Object.is(+0, -0) 为 false)。
注意:Object.is 不是类型转换工具,它比 === 更严格,也更语义准确——它定义的是“SameValue 零值相等算法”,专为精确值比较而生。
直接用 Set 无法处理 NaN,因为 Set 内部用的是 SameValueZero 算法(和 === 行为一致),不是 SameValue。必须手动实现去重逻辑:
常见做法是遍历 + Object.is 判重:
function uniqueByObjectIs(arr) { const seen = []; return arr.filter(item => { const notExists = !seen.some(seenItem => Object.is(seenItem, item)); if (notExists) seen.push(item); return notExists; });}<p>// 示例uniqueByObjectIs([1, NaN, 2, NaN, 0, -0]); // [1, NaN, 2, 0, -0]
要点:
seen.some(...) 必须用 Object.is,不能用 === 或 ==
0 和 -0 会被视为不同项,符合预期(若业务需要合并,得额外判断)Map 存储已见值的字符串化键(但要注意 NaN 序列化后都是 "NaN",反而会把多个 NaN 合并——所以仍需自定义键生成逻辑)如果数组很大,又必须支持 NaN、+0/-0 精确区分,可以借助 Map 实现 O(n) 去重,关键是设计能区分这些值的键:
一个稳妥的 key 生成方式:
function getUniqueKey(val) { if (Number.isNaN(val)) return 'NaN@' + Math.random().toString(36).slice(2, 8); if (val === 0 && 1 / val === -Infinity) return '−0'; return String(val);}
但这个方案有缺陷:每次调用 getUniqueKey(NaN) 生成不同 key,破坏一致性。真正可靠的写法是用 Object.is 手动维护键集合,或接受 NaN 在 Map 中只能存一个(因 Map 的 has 方法仍基于 SameValueZero)。所以——
filter + some + Object.is 更清晰可靠 JSON.stringify 或 String() 作为去重 key,它们对 NaN、undefined、function 等都失效 Map 本身不解决 NaN 键重复问题,它的 set 和 has 仍遵循 SameValueZero有人会写:[...new Set(arr.map(x => Object.is(x, x) ? x : x))],这毫无意义——Object.is(x, x) 永远为 true,没改变原始值。
另一个常见错误是混用比较目标:
// ❌ 错误:拿 Object.is 去比较索引和值arr.filter((v, i) => Object.is(v, i))<p>// ✅ 正确:只用于值与值之间比较arr.filter((v, i) => !seen.some(s => Object.is(s, v)))
还有人试图覆盖 Array.prototype.includes 的行为,但该方法内部固定使用 SameValueZero,无法被 Object.is 替代——必须手写逻辑。
最易被忽略的一点:Object.is 是浅比较,对对象或数组仍然无能为力;如果你的“数值”其实是包装对象(如 new Number(NaN)),Object.is 也会返回 false,因为它比的是引用而非包装值。