前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库Object.is
JSJavaScript相等比较

Object.is、=== 与 SameValueZero 的差别

Object.is 与 === 只在 NaN 和正负零上不同;Set 与 Map 使用的 SameValueZero 又合并了正负零。三者都不做对象深比较。

前端进阶之旅 · 一题精讲更新于 2026.09.05
JavaScript#相等比较#缓存
先看核心答案读代码示例
理解线索

三种规则一眼区分

  1. 严格等于 ===不转型;NaN 不等于自身;+0 等于 -0
  2. Object.isNaN 等于自身;区分 +0 与 -0
  3. SameValueZero像 Object.is,但把 +0 和 -0 视为同键

不要把 Object.is 理解成“更严格的 ===”;它只是换了两条边界规则。

核心回答

先记住这个答案

=== 不做类型转换,但认为 NaN 不等于自身,且把 +0 和 -0 看作相等。Object.is 认为 NaN 等于 NaN,并区分 +0 与 -0。Set、Map 的键用 SameValueZero:接近 Object.is,唯一关键差别是把 +0 和 -0 合并。三者比较对象时都只看身份,不递归比较属性内容。

  • 先问要按值、身份还是键去重
  • NaN 与正负零是三规则分水岭
  • 对象比较只看身份,不做深比较

真正分叉的边界只有几处

日常比较里,=== 已经覆盖大多数场景:类型不同直接 false,类型相同再按原始值或对象身份判断。它没有 == 那种类型转换,所以 1 === '1' 是 false。问题在于两个角落:NaN 被规定为不等于任何值,包括它自己;而 +0 和 -0 在数学意味上常被当作同一个零,=== 也沿用了这个直觉。

Object.is(NaN, NaN) 返回 true,Object.is(0, -0) 返回 false。这不是一种全面“更严格”的比较:它在 NaN 上判为相同,在正负零上判为不同。对其他值,结果与严格相等一致。正负零可在部分数值计算中产生不同结果,例如倒数分别为正无穷与负无穷,因此是否保留这种区别应由业务数据的含义决定。

SameValueZero 主要出现在 Set、Map 的键比较,以及数组 includes 这类“是否已经存在”的判断里。它的效果接近 Object.is,会让重复 NaN 只保留一个,但它刻意把 +0 和 -0 视为同一个键。原因很简单:集合去重通常关心业务意义上的同一个数,而不是零的符号。

三组边界输出JavaScript
console.log(NaN === NaN, Object.is(NaN, NaN));
console.log(0 === -0, Object.is(0, -0));
console.log(new Set([NaN, NaN, 0, -0]).size);
console.log(Object.is({ n: 1 }, { n: 1 }));
查看输出与解释
false true
true false
2
false

前两行显示 NaN 与正负零在 === 和 Object.is 下结论相反;Set 用 SameValueZero,所以 NaN 去重、正负零合并,只剩两个键;两个新对象属性相同也不是同一个身份。

对象内容相同不等于同一个对象

无论 ===、Object.is 还是 SameValueZero,遇到对象、数组、函数、Map、Set 实例时都不会展开属性逐项比较。{ n: 1 } 和另一个 { n: 1 } 在内存里是两个身份,所以结果一定是 false。能把它们判等,除非你手里保存的是同一个引用,例如 const a = { n: 1 }; const b = a。

这解释了为什么深比较要单独写:递归遍历字段、处理循环引用、区分数组顺序和日期时间,都不是这三种规则承诺的能力。把 Object.is(a, b) 当成“结构相同”会让缓存失效、依赖判断和状态更新逻辑一起失真。反过来,如果你确实只想知道引用有没有换,Object.is 足够清晰,也比递归遍历便宜得多。

不可变更新利用的正是身份规则。你保留旧对象,创建一个新对象表示变化,UI 层就能用引用是否相同快速判断“这一块可能变了”。但它只回答“引用换没换”,不回答“业务语义变没变”;如果每次渲染都新建一个内容相同的配置对象,身份比较会认为发生了变化。

业务里要先定义“相同”的含义

先规定无效数值与正负零在业务中的含义,再选择比较规则。例如坐标计算产生 NaN 时,通常应先校验输入并明确报错或缺失策略;如果允许将 NaN 作为集合元素,Set 会把多个 NaN 合并。需要保留每次失败的独立记录时,应给记录建立独立标识,而不是期待更换这三种比较规则就能区分不同的 NaN。

Map 使用 SameValueZero:NaN 可以命中此前的 NaN 键,0 与 -0 会命中同一个键。对象键按身份匹配,两个内容相同的新对象不会自动命中。若要按对象内容缓存,需要设计覆盖必要字段、稳定且无歧义的缓存键;简单 JSON 序列化还需考虑字段顺序、不可序列化数据和碰撞边界。

不可变更新通过创建新引用表达状态变化,但新引用也可能装着相同内容。React 比较部分状态和依赖值时使用 Object.is,不会替应用递归检查对象字段。因此应从正确的数据所有权和依赖关系出发,减少无意义的新引用,而不是把身份比较误当成内容比较或缓存命中保证。

数值数据
先约定 NaN、缺失值和正负零各自的业务含义。相等规则只负责比较,不能代替校验,也无法恢复已经在计算或序列化中丢失的信息。
缓存与去重
对象身份与业务键是不同的索引依据。需要跨对象实例命中时,应使用经过设计的稳定键,并明确更新、失效和碰撞处理规则。

框架里的 Object.is 只是身份与边界值判断

在 React 里看到 Object.is,不要把它读成“框架替你做了深比较”。例如 useState 的更新函数若返回与当前值 Object.is 相同的结果,通常可以 bail out;但你传入一个新建的 { n: 1 },即使字段一样,Object.is 仍是 false。effect 依赖数组也类似,它逐项用相同值规则看引用或原始值是否变化,对象字面量每轮新建就会被视为变化。

这也不是“只要相同就永远不再执行任何组件代码”的保证。调度、并发渲染、父组件更新、上下文变化都可能让函数组件仍然被调用,只是某些 state 更新或 effect 重跑被避免。工程上更稳的写法是减少无意义的新引用:把稳定配置提到组件外,用 useMemo 或 useCallback 固定身份,或者在状态层保存可比较的原始值。

当你必须比较结构,请明确引入深比较并控制成本,而不是期待 Object.is 自动理解数据。选择规则前先写一句话:相同是按身份、按 SameValueZero 键,还是按归一化后的字段。写下来之后,===、Object.is、Set/Map 和深比较各自的位置就清楚了。

回答前,多想一步

容易答错的地方

把 Object.is 说成更严格的 ===
二者没有高低级,只是边界不同:=== 合并正负零、否定 NaN 自等;Object.is 识别 NaN 自等并区分正负零。
以为 Set 会分开保存 0 和 -0
Set 与 Map 键用 SameValueZero,NaN 会去重,+0 和 -0 也会合并成一个键,所以示例 size 是 2。
用 Object.is 判断两个对象内容一致
对象比较只看身份;属性全同的新对象仍不等。需要内容一致就做深比较或先建立稳定引用。排查“Object.is”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
试着用自己的话回答

面试官还会怎么问?

什么时候应该优先用 Object.is?

需要把 NaN 当作可匹配值,或必须区分 +0 与 -0 时使用;普通等值判断仍多用 ===。

Array.prototype.includes 用哪种规则?

它按 SameValueZero 查找,所以 [NaN].includes(NaN) 为 true,[0].includes(-0) 也为 true。

为什么 Map 用对象作键经常查不到?

因为键按身份而不是字段内容比较。两个看起来相同的对象不是同一键,应改用稳定字符串键或复用同一引用。

从一道题,走向一组知识

把知识连起来

数组与集合

Array.prototype.sort 会修改原数组吗?如果不想修改原数组该怎么做?

sort 返回同一数组引用,复制后排序则返回新数组,适合用身份比较核对差别。

对象与原型

enumerable 如何影响 for...in 和 Object.keys?

比较对象内容时还需决定检查哪些属性,枚举性与对象身份回答的是不同问题。

放回知识体系中复习JavaScript 进阶

参考资料

  • MDN · 相等比较与相同值比较

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 真正分叉的边界只有几处
  3. 对象内容相同不等于同一个对象
  4. 业务里要先定义“相同”的含义
  5. 框架里的 Object.is 只是身份与边界值判断
  6. 容易答错的地方
  7. 面试官还会怎么问
  8. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑