◆

JavaScript Object.is 判断值相等详解

2023-02-02 01:370

Object.is()

ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。 它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及 +0 等于 -0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

Object.is 用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是 +0 不等于 -0,二是 NaN 等于自身。

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

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

三套相等语义

规范里其实定义了三种不同的相等判断,搞清它们的分工就不容易记混:

  1. 宽松相等(==),规范里叫 Abstract Equality。类型不同时会按规则做隐式转换再比,'1' == 1、null == undefined、[] == false 都是 true。转换规则细节多,所以日常代码里我们基本不用它。
  2. 严格相等(===),Strict Equality。类型不同直接返回 false,不做转换。但它对 NaN 和零值有两个特例。
  3. 同值相等(Object.is),SameValue。可以理解成「修好了那两个特例的 ===」。

Object.is 与 === 的差异只有前面提到的两处,其余行为完全一致,包括对象仍然按引用比较:

Object.is({}, {}); // false,两个不同的对象
Object.is('1', 1); // false,不做类型转换
Object.is(null, undefined); // false

const o = {};
Object.is(o, o); // true

NaN 为什么不等于自身

NaN !== NaN 不是 JavaScript 的设计失误,而是照抄了 IEEE 754 浮点标准:NaN 表示「不是一个数」,它可能来自 0/0、Math.sqrt(-1)、parseInt('abc') 等各种失败的运算,两个都失败了的结果之间并没有理由相等。

代价是判断一个值是不是 NaN 变得别扭。历史上的做法是全局 isNaN,但它会先做类型转换,isNaN('abc') 会返回 true,这基本不是我们想问的问题。ES6 之后有三个更准确的选择:

isNaN('abc'); // true,先转成数字再判断,容易误判
Number.isNaN('abc'); // false,只有真正的 NaN 才是 true
Number.isNaN(NaN); // true
Object.is(NaN, NaN); // true
[NaN].includes(NaN); // true,includes 用的也是 SameValueZero

+0 与 -0 的区别在哪

浮点数用一个独立的符号位表示正负,所以 0 有两个:+0 和 -0。它们在几乎所有运算里表现相同,+0 === -0 也是 true,因此平时感觉不到差别。会露出来的地方主要是除法的符号和 Math 的取整结果:

1 / +0; // Infinity
1 / -0; // -Infinity
Math.round(-0.4); // -0
(-0).toString(); // "0",字符串化看不出区别

Object.is(-0, 0); // false,这是唯一直接区分它们的内建方法

真正需要区分正负零的场景不多,做几何、动画方向或者物理计算时可能会碰到——比如一个速度值降到 -0,我们想知道它原本朝哪个方向。这时候 Object.is(v, -0) 是最直接的判断。

SameValueZero:数组和 Map 用的第四套规则

还有一个规范内部使用、没有暴露成方法的比较算法叫 SameValueZero,它和 Object.is 只差一点:认为 +0 等于 -0,但 NaN 等于自身。Array.prototype.includes、Map 和 Set 的键去重用的都是它,这解释了为什么 Set 里放两个 NaN 只会留下一个:

new Set([NaN, NaN]).size; // 1,NaN 被当成同一个键
new Set([0, -0]).size; // 1,+0 与 -0 也被当成同一个

[NaN].indexOf(NaN); // -1,indexOf 用的是 ===,所以找不到
[NaN].includes(NaN); // true,includes 用的是 SameValueZero

indexOf 和 includes 的这个差异是很实际的坑:数组里可能出现 NaN 时,判断存在性一定要用 includes。

该用哪个

日常写业务代码,=== 依然是默认选择,可读性最好也最符合直觉。只有在下面这些情况才需要换:

  • 判断是不是 NaN → 用 Number.isNaN,语义最清楚。
  • 写通用的深比较、memo 缓存键、状态是否变化的判断 → 用 Object.is,因为这类逻辑不希望 NaN 和自己不相等导致误判「变了」。React 的 useState 判断是否需要重渲染用的就是 Object.is。
  • 需要区分正负零 → 只有 Object.is 能做到。

顺带说一句,Object.is 名字里带 Object 只是因为它挂在 Object 上作命名空间,它和对象没有特别关系,比较原始值同样适用。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEJavaScript Object.is 判断值相等详解加载中