Object.is() 是什么?和 === 有什么区别?

Object.is 不做类型转换,采用 SameValue 语义:它认为 NaN 等于自身,并区分 +0-0

先给出面试版结论:Object.is(a, b) 用于判断两个值是否是“同一个值”。它不会进行隐式类型转换;对于对象仍然比较引用。它和 === 只有两个关键差异:Object.is(NaN, NaN)trueObject.is(+0, -0)false

Object.is(NaN, NaN); // true
Object.is(+0, -0);   // false

NaN === NaN; // false
+0 === -0;   // true

1. 基本用法

Object.is(value1, value2);

返回一个布尔值。它不会把字符串转成数字,也不会把对象转成原始值:

Object.is(1, 1);       // true
Object.is("1", 1);     // false
Object.is(null, null); // true
Object.is(null, undefined); // false

Object.is({}, {}); // false

const user = { name: "Ada" };
Object.is(user, user); // true

对于大多数普通值,Object.is=== 的结果一致:

Object.is("hello", "hello"); // true
Object.is(10n, 10n);         // true
Object.is(Symbol.for("id"), Symbol.for("id")); // true

它虽然挂在 Object 上,但参数不必是对象,也不会对参数进行装箱后再比较。

2. 与 ===== 的完整对比

比较方式规范中的相等语义隐式类型转换NaN 与自身+0-0对象比较
==Abstract Equalityfalsetrue转换或比较引用
===Strict Equality不会falsetrue比较引用
Object.isSameValue不会truefalse比较引用
"0" == 0;             // true:发生类型转换
"0" === 0;            // false
Object.is("0", 0);    // false

NaN == NaN;           // false
NaN === NaN;          // false
Object.is(NaN, NaN);  // true

+0 == -0;             // true
+0 === -0;            // true
Object.is(+0, -0);    // false

因此不能简单说 Object.is 是“更严格的 ===”。它采用的是另一套相等语义:对 NaN 更宽松,对正负零更严格。

3. 为什么 NaN 能等于自身?

NaN 表示某次数值运算没有得到有效数值。按照 IEEE 754 和 JavaScript 的相等规则,使用 ===== 时,NaN 不等于任何值,包括它自己:

const result = Number("not a number");

result === NaN; // false
result !== result; // true

SameValue 语义特意把两个 NaN 视为相同,所以 Object.is 可以判断一个值是不是 NaN

Object.is(result, NaN); // true

不过表达业务意图时,通常优先使用 Number.isNaN

Number.isNaN(result); // true

不要使用全局 isNaN 代替它,因为全局 isNaN 会先进行数值转换:

isNaN("hello");        // true
Number.isNaN("hello"); // false

4. 为什么要区分 +0-0

JavaScript 的 Number 使用 IEEE 754 双精度浮点数,零也带有符号位,因此存在 +0-0。它们在大多数运算中表现相同,但并非完全不可区分:

1 / +0; // Infinity
1 / -0; // -Infinity

Object.is(+0, -0); // false

负零可以来自保留方向信息的计算:

Math.sign(-0); // -0
Math.round(-0.1); // -0

在绝大多数业务场景中不需要区分正负零;数值算法、坐标方向、符号传播等场景才可能需要 SameValue 的这项特性。

判断负零可以写成:

function isNegativeZero(value) {
  return value === 0 && 1 / value === -Infinity;
}

isNegativeZero(-0); // true
isNegativeZero(+0); // false

5. 对象仍然比较引用

Object.is 不会深度比较对象内容:

Object.is({ x: 1 }, { x: 1 }); // false
Object.is([1, 2], [1, 2]);     // false

两个对象字面量会创建两个不同对象,即使属性完全相同,也不是同一个引用。只有两边指向同一个对象时才返回 true

const point = { x: 1 };
const alias = point;

Object.is(point, alias); // true

完整链路是:

输入两个值

类型或普通值不同 → false

都是对象 → 是否为同一引用

都是 Number → 特殊处理 NaN 和正负零

其余相同值 → true

如果业务需要比较对象结构,应根据数据模型逐字段比较,或选择适合的深比较策略;Object.is 不负责这件事。

6. SameValue、Strict Equality 和 SameValueZero

JavaScript 不只有一套“相等”算法。常见的三套语义是:

算法典型 API/语法NaN 与自身+0-0
Strict Equality===Array.prototype.indexOf不相等相等
SameValueObject.is相等不相等
SameValueZeroArray.prototype.includesMapSet相等相等

SameValueZero 可以理解为:像 Object.is 一样识别 NaN,但不区分正负零。

[NaN].indexOf(NaN);  // -1:使用 Strict Equality
[NaN].includes(NaN); // true:使用 SameValueZero

const set = new Set([+0, -0, NaN, NaN]);
set.size; // 2:正负零算一个,NaN 也只算一个

这解释了为什么不同 API 面对相同数据可能给出不同结果。回答这类题时,不要只说“底层用了 Object.is”;更准确的做法是指出具体 API 采用哪种相等算法。

7. 一个最小实现

下面是 Object.is 的常见等价实现,用于理解两个特殊分支:

function sameValue(x, y) {
  if (x === y) {
    // 普通相等值直接为 true;零还要通过倒数符号区分 +0 和 -0
    return x !== 0 || 1 / x === 1 / y;
  }

  // 只有 NaN 是 JavaScript 中不等于自身的值
  return x !== x && y !== y;
}

sameValue(NaN, NaN); // true
sameValue(+0, -0);   // false
sameValue({}, {});   // false

它的执行过程是:

  1. 先用 === 处理绝大多数情况;
  2. 如果两者严格相等且为零,再比较倒数,区分 Infinity-Infinity
  3. 如果不严格相等,检查两边是否都具有 NaN 的“不等于自身”特征。

这是符合常见 polyfill 思路的教学实现,不是在复刻某个 JavaScript 引擎的内部源码。现代引擎可能直接使用内部类型标签、浮点位或优化后的指令完成判断。

8. Object.is 在 React 中的作用

React 的多处比较语义与 Object.is 有关。例如 Hooks 的依赖项会逐项用 Object.is 比较:

useEffect(() => {
  synchronize(value);
}, [value]);

可以简化理解为:

本次 value 与上次已提交的 value
              ↓ Object.is
相同 → 该依赖没有变化
不同 → effect 需要重新同步

这会产生两个容易忽略的边界:

// NaN → NaN:Object.is 为 true,不会仅因此重新执行
// +0 → -0:Object.is 为 false,会被视为依赖变化

useMemouseCallback 的依赖比较也采用相同思路。React.memo 默认对每个 prop 使用 Object.is 语义进行浅比较,所以新对象、新数组和新函数仍会被视为变化。

这里的重点不是背“React 用了 Object.is”,而是理解它只比较一层引用:

const options = { theme: "dark" };

// 下一次 render 又创建新对象时:
Object.is(previousOptions, options); // false

9. 边界与常见陷阱

陷阱一:以为它会深比较

Object.is({ id: 1 }, { id: 1 }); // false

Object.is 比较对象身份,不遍历属性。

陷阱二:用它比较浮点计算误差

Object.is(0.1 + 0.2, 0.3); // false

它不会修复 IEEE 754 精度问题。浮点近似比较通常要结合业务量级设置容差:

function nearlyEqual(a, b, tolerance = Number.EPSILON) {
  return Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
}

固定使用 Number.EPSILON 也不适合所有数量级,容差必须由业务精度要求决定。

陷阱三:认为它会发生类型转换

Object.is(new Number(1), 1); // false
Object.is("1", 1);           // false

包装对象是对象,原始数字是原始值;两者不是同一个值。

陷阱四:混淆 isNaNObject.is

Object.is(value, NaN) 可以准确识别 NaN,但 Number.isNaN(value) 意图更清晰。全局 isNaN 会进行类型转换,语义不同。

陷阱五:认为 includes 内部就是调用 Object.is

二者在 NaN 上表现相同,但 includes 使用 SameValueZero,不区分 +0-0,不能直接画等号。

陷阱六:看到 Object.is 就替换所有 ===

大多数业务比较用 === 更符合直觉,也更常见。只有明确需要把 NaN 当作相同值、区分正负零,或需要遵循某个 API 的 SameValue 语义时,才有必要主动选择 Object.is

10. 面试官递进追问

1. Object.is 是做什么的?

它使用 SameValue 语义比较两个值,不做类型转换;对象比较引用。与 === 的差异只有 NaN 和正负零两组边界。

为什么问: 检查基础定义。回答要先给共同点,再给两个差异。

2. Object.is 是否比 === 更严格?

不宜这样概括。它对正负零区分得更细,但把两个 NaN 视为相同,因此是不同语义,而不是单向“更严格”。

为什么问: 检查是否只会背口号。回答应抓住两个方向相反的差异。

3. Object.is({}, {}) 为什么是 false

两个对象字面量创建了不同对象。SameValue 对对象比较身份或引用,不比较属性结构。

为什么问: 检查是否混淆同值比较与深比较。

4. 如何不用 Object.is 区分 +0-0

先确认值等于零,再比较倒数:1 / +0Infinity1 / -0-Infinity

为什么问: 检查是否理解实现中的特殊分支,而不只是记输出。

5. Number.isNaNObject.is(value, NaN) 有什么区别?

判断单个值是否为 NaN 时,结果基本一致且都不做字符串到数字的强制转换;Number.isNaN 表意更直接。Object.is 是通用的两值 SameValue 比较。

为什么问: 检查 API 选择和代码可读性意识。

6. includes 为什么能找到 NaNindexOf 却不能?

includes 使用 SameValueZero,认为 NaN 与自身相等;indexOf 使用 Strict Equality,NaN === NaNfalse

为什么问: 检查能否把知识迁移到标准库,而不是误以为所有比较都用 ===

7. React 为什么会受 Object.is 的对象引用规则影响?

Hook 依赖和默认 memo props 比较只看每个值是否 SameValue。每次 render 创建的新对象或函数引用不同,即使内容看起来一样,也会被判定为变化。

为什么问: 检查语言基础与框架机制能否连起来。

8. Object.is 能解决 0.1 + 0.2 的问题吗?

不能。它只定义如何比较已有值,不改变浮点数的表示和计算。近似比较需要按业务精度设计容差,金额等精确十进制场景应选择整数最小单位或十进制库。

为什么问: 检查是否混淆相等算法与数值精度。

9. 什么时候应该在项目中主动使用它?

需要准确识别 NaN、区分正负零、实现 SameValue 语义的工具,或解释 React 等 API 的变化判断时使用。普通业务相等判断通常仍优先 ===

为什么问: 检查能否把理论转成工程决策,并考虑可读性。

11. 常见错误回答

  • Object.is 就是严格版 ===。” 不准确,因为它对 NaN 反而给出相等结果。应说两者采用不同相等语义。
  • Object.is 可以比较对象内容。” 错误。对象仍按引用比较。
  • Object.is 不会出现浮点误差。” 错误。它不参与数值计算,只比较计算后的值。
  • Object.is 判断 NaN 时等同于全局 isNaN。” 错误。全局 isNaN 会先做类型转换。
  • Setincludes 都使用 Object.is。” 不严谨。它们使用 SameValueZero,不区分正负零。
  • “React 依赖比较会深度检查对象。” 错误。对象引用变化就会被视为依赖变化。

12. 最终记忆框架

核心关键词:SameValue、不做类型转换、NaN、正负零、对象引用

一句话本质:Object.is 是 JavaScript 的 SameValue 比较入口,主要修正 ===NaN 和正负零的两项特殊语义。

Object.is(a, b)

普通原始值:像 ===
对象:比较同一引用
NaN 与 NaN:true
+0 与 -0:false

一分钟回答:说明不做类型转换、对象比较引用,以及与 === 的两个差异。

三分钟回答:补充 SameValue、Strict Equality、SameValueZero 的对比,以及 includesSet 和 React 的典型应用。

十分钟回答:从教学版实现讲到 NaN、IEEE 754 正负零、对象身份、React Hook 依赖比较、浮点误差与深比较的边界,再说明工程中何时选择 ===Object.is

这套理解还可以迁移到 Map/Set 去重、数组查找、React 依赖数组、不可变数据、引用稳定性和浮点数精度问题。面试时始终先问:当前 API 采用哪种相等算法?它是否转换类型、如何处理 NaN 和正负零、对象按值还是按引用?