【相等性比较的通解——理解 JavaScript 中的 Object.is()】
相等性比较的通解——理解 JavaScript 中的 Object.is()
一、引言
在 JavaScript 中,相等性比较一直是一个复杂的话题。ES6 引入的 Object.is()
方法为我们提供了一种新的相等性判断机制,它解决了 ==
和 ===
运算符的一些历史遗留问题。本文将深入探讨 Object.is()
的方方面面。
二、历史背景
Object.is()
是在 ECMAScript 6 (ES2015) 中引入的,其提出过程如下:
- 2013年:首次在 ES6 规范草案中提出
- 2015年:随 ES6 正式发布
- 目的:提供一种更严格的相等性比较机制,称为"Same-value equality"(同值相等)
主流浏览器支持时间线:
- Chrome: 30+ (2013年9月)
- Firefox: 29+ (2014年4月)
- Safari: 9+ (2015年9月)
- Edge: 12+ (2015年7月)
三、Object.is() 的实现原理
3.1 基本实现
Object.is()
的 polyfill 实现揭示了其核心原理:
if (!Object.is) {Object.is = function(x, y) {// 处理 NaNif (x !== x) {return y !== y;}// 处理 +0 和 -0if (x === 0 && y === 0) {return 1 / x === 1 / y;}// 其他情况return x === y;};
}
3.2 特殊情况处理
Object.is()
主要解决了两个特殊情况:
-
NaN 的比较:
Object.is(NaN, NaN) // true NaN === NaN // false
-
零值的比较:
Object.is(+0, -0) // false +0 === -0 // true
四、与其他比较方式的对比
JavaScript 中存在四种主要的相等性比较方式:
// 1. == (Abstract Equality Comparison)
'' == false // true(存在类型转换)
1 == '1' // true// 2. === (Strict Equality Comparison)
'' === false // false
+0 === -0 // true
NaN === NaN // false// 3. Object.is (Same-value equality)
Object.is('', false) // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true// 4. Same-value-zero equality (Array.includes)
['foo'].includes('foo') // true
[NaN].includes(NaN) // true
[+0].includes(-0) // true
五、基本类型的比较
5.1 为什么选择 Object.is
Object.is
是比较基本类型最靠谱的方式,原因如下:
- 处理了 JavaScript 中的特殊数值情况(+0/-0, NaN)
- 不进行类型转换,避免了意外结果
- 符合直觉的相等性判断
- 在处理基本类型时提供了最严格和准确的比较
- 被 ECMAScript 规范采用作为"同值相等"的标准实现
- 在现代框架(如 React)中被广泛使用
5.2 null 和 undefined 的特殊性
null
和 undefined
在 Object.is
中的处理很特别:
// 它们与自身比较
Object.is(null, null) // true
Object.is(undefined, undefined) // true// 它们互相比较
Object.is(null, undefined) // false
这是因为它们各自都是其类型的唯一值:
null
是 Null 类型的唯一值undefined
是 Undefined 类型的唯一值
六、在现代框架中的应用
6.1 React 中的应用
// useState 的内部实现
function basicStateReducer(state, action) {return Object.is(state, action) ? state : action;
}// React.memo 的默认比较函数
function areEqual(prevProps, nextProps) {return Object.is(prevProps, nextProps);
}
6.2 Vue 3 中的应用
// Vue 3 响应式系统
function hasChanged(value, oldValue) {return !Object.is(value, oldValue);
}
七、性能考虑
Object.is
的性能与 ===
相近,在大多数现代引擎中已经被优化。但在某些场景下需要注意使用方式:
// 不推荐(性能不好)
array.findIndex(item => Object.is(item, searchValue))// 推荐
array.findIndex(item => item === searchValue) // 对于普通值
array.findIndex(item => Number.isNaN(item)) // 对于 NaN
八、最佳实践
8.1 数值比较
// 精确的零值比较
function isNegativeZero(value) {return Object.is(value, -0);
}// NaN 检测
function isReallyNaN(value) {return Object.is(value, NaN);
}
8.2 状态比较
// 状态更新检测
function hasStateChanged(oldState, newState) {return !Object.is(oldState, newState);
}
8.3 类型检查
// null 或 undefined 检查
function isNullOrUndefined(value) {return Object.is(value, null) || Object.is(value, undefined);
}
九、实际应用场景
9.1 数据验证
function validateNumber(value) {if (Object.is(value, NaN)) {throw new Error('Invalid number');}if (Object.is(value, -0)) {// 特殊处理负零的情况return +0;}return value;
}
9.2 状态管理
class StateManager {#state = {};setState(newState) {if (!Object.is(this.#state, newState)) {this.#state = newState;this.notifyUpdate();}}
}
9.3 缓存优化
const memoize = (fn) => {const cache = new Map();return (...args) => {const key = JSON.stringify(args);const cached = cache.get(key);if (cached && Object.is(cached.args, args)) {return cached.result;}const result = fn(...args);cache.set(key, { args, result });return result;};
};
十、总结
Object.is()
的引入标志着 JavaScript 在处理相等性比较方面的重要进步。它提供了比 ===
更严格的相等性比较,解决了 NaN
和 ±0
的比较问题,并已被现代框架广泛采用。理解和正确使用 Object.is()
对于编写可靠的 JavaScript 代码至关重要。
在实际开发中,我们应该根据具体场景选择合适的比较方式:
- 需要严格的值比较时,使用
Object.is()
- 需要考虑性能的普通比较时,使用
===
- 需要类型转换的比较时,使用
==
通过深入理解 Object.is()
,我们能够在代码中更好地处理相等性比较,提高代码的可靠性和可维护性。
附录:数组里性能差异原因
性能差异分析:Object.is
vs ===
函数调用开销
// 版本1:使用 Object.is
array.findIndex(item => Object.is(item, searchValue))// 版本2:使用 ===
array.findIndex(item => item === searchValue)
主要区别
Object.is
是一个函数调用,每次比较都需要创建一个新的函数调用栈帧===
是语言内置的运算符,直接在 CPU 层面执行
执行步骤对比
// 使用 Object.is 时的步骤
array.findIndex(item => {// 1. 创建函数调用栈帧// 2. 传递参数// 3. 执行 Object.is 的内部逻辑// 4. 返回结果// 5. 销毁栈帧return Object.is(item, searchValue);
});// 使用 === 时的步骤
array.findIndex(item => {// 1. 直接执行 CPU 级别的比较操作return item === searchValue;
});
性能测试示例
const arr = new Array(1000000).fill(1);
const searchValue = 2;console.time('Object.is');
arr.findIndex(item => Object.is(item, searchValue));
console.timeEnd('Object.is');console.time('===');
arr.findIndex(item => item === searchValue);
console.timeEnd('===');// 典型输出:
// Object.is: 8.123ms
// ===: 3.456ms
更详细的性能分析
// 1. 单次操作的性能比较
console.time('Single Object.is');
Object.is(1, 2);
console.timeEnd('Single Object.is');console.time('Single ===');
1 === 2;
console.timeEnd('Single ===');// 2. 循环中的性能比较
console.time('Loop Object.is');
for(let i = 0; i < 1000000; i++) {Object.is(1, 2);
}
console.timeEnd('Loop Object.is');console.time('Loop ===');
for(let i = 0; i < 1000000; i++) {1 === 2;
}
console.timeEnd('Loop ===');
在不同场景下的影响
// 场景1:单次比较
const a = 1, b = 2;
Object.is(a, b); // 性能影响可以忽略
a === b; // 性能最优// 场景2:大数组遍历
const largeArray = new Array(1000000).fill(1);// 不推荐:每次迭代都要调用函数
largeArray.findIndex(item => Object.is(item, 2));// 推荐:直接使用运算符
largeArray.findIndex(item => item === 2);
特殊情况的处理
// 对于 NaN 的查找
const arrayWithNaN = [1, NaN, 2, 3];// 方法1:使用 Object.is(虽然准确但性能较差)
arrayWithNaN.findIndex(item => Object.is(item, NaN));// 方法2:使用 Number.isNaN(更优的方案)
arrayWithNaN.findIndex(item => Number.isNaN(item));
优化建议
// 不好的实现
function findInArray(arr, value) {return arr.findIndex(item => Object.is(item, value));
}// 更好的实现
function findInArray(arr, value) {// 对特殊情况进行处理if (Number.isNaN(value)) {return arr.findIndex(Number.isNaN);}if (Object.is(value, -0)) {return arr.findIndex(item => Object.is(item, -0));}// 普通情况使用 ===return arr.findIndex(item => item === value);
}
总结
- 虽然单次
Object.is
和===
的性能差异很小,但在大量重复调用(如数组方法)中,这个差异会被放大 - 函数调用的开销(创建栈帧、传参、返回值等)是造成性能差异的主要原因
- 在循环或数组方法中,应优先使用
===
,除非确实需要Object.is
的特殊处理(如NaN
或±0
的比较) - 对于特殊值的比较,可以使用更专门的方法(如
Number.isNaN
)来获得更好的性能
这就是为什么在数组方法中推荐使用 ===
而不是 Object.is
的原因。这是在准确性和性能之间做出的权衡。
为什么要出现Object.is()这个函数?
Object.is
的出现原因及其与 ===
的区别
Object.is
主要解决的问题
// 1. NaN 的比较问题
NaN === NaN // false
Object.is(NaN, NaN) // true// 2. 零值的区分问题
+0 === -0 // true
Object.is(+0, -0) // false
为什么要专门出一个 Object.is
?
主要原因
- 提供一个更符合数学概念的相等性比较方法
- 解决 JavaScript 中一些历史遗留的特殊情况
- 在语言层面统一相等性比较的标准
具体场景分析
// 1. NaN 的问题
// 在数学中,NaN 应该等于自身,但在 JS 中:
NaN === NaN // false(这是一个反直觉的结果)
isNaN(NaN) // true(传统解决方案)
Number.isNaN(NaN) // true(ES6 解决方案)
Object.is(NaN, NaN) // true(更直观的方案)// 2. 零值的问题
// 在某些数学计算中,+0 和 -0 的区别很重要:
1 / +0 // Infinity
1 / -0 // -Infinity// === 无法区分这种情况:
+0 === -0 // true// Object.is 可以区分:
Object.is(+0, -0) // false
在实际应用中的意义
// 1. 在数学计算中
function divideByZero(value) {// 这里区分 +0 和 -0 是有意义的if (Object.is(value, -0)) {return '-Infinity';}if (Object.is(value, +0)) {return 'Infinity';}
}// 2. 在数值验证中
function validateNumber(value) {if (Object.is(value, NaN)) {throw new Error('Invalid number');}// 其他验证逻辑
}// 3. 在现代框架中(如 React)
function shouldComponentUpdate(nextProps) {// 使用 Object.is 可以更准确地判断值是否真的改变return !Object.is(this.props.value, nextProps.value);
}
为什么不直接修改 ===
的行为?
主要考虑
- 向后兼容性 - 修改
===
的行为会破坏大量现有代码 - 性能考虑 -
===
作为基础运算符需要保持高性能 - 渐进式改进 - 通过新的 API 来提供更好的解决方案
使用建议
// 1. 普通值比较,使用 ===
if (value === 42) { ... }// 2. 需要准确判断 NaN 时
if (Object.is(value, NaN)) { ... }
// 或者更好的方式
if (Number.isNaN(value)) { ... }// 3. 需要区分 +0 和 -0 时
if (Object.is(value, -0)) { ... }// 4. 在框架或库的实现中,需要严格的值比较时
if (Object.is(oldValue, newValue)) { ... }
总结
Object.is
的主要目的是提供一个更符合数学直觉的相等性比较方法- 它主要解决了
NaN
和±0
的比较问题 - 对于
null
和undefined
的处理,它与===
是一致的 - 它的出现不是为了替代
===
,而是提供一个更严格的相等性比较选项
在实际开发中,应该根据具体需求选择合适的比较方式:
- 普通比较用
===
- 需要严格数学比较时用
Object.is
- 特殊值检测用专门的方法(如
Number.isNaN
)
这样设计的好处是既保持了语言的向后兼容性,又提供了更准确的值比较方案,同时还保留了不同场景下的性能优化空间。