JavaScript 中的 “相等” 算法
整理笔记本时,看到以往一些对 JavaScript 中 “相等” 关系的记录。 尽管这是一个被说烂了的话题,但写一篇资料整理总结,当作复习下也不错。
JavaScript 的 “相等” 比较算法,按严格程度来排,有以下几种:
==双等号===三等号SameValueZero算法SameValue算法
下面逐个介绍它们的特点。
== 双等号
在 JavaScript 中,使用 == 来判断相等,是一种不严格的比较。
关于双等号,最广为人知的一点,就是在比较不同类型的值时,JavaScript 会进行自动转型。
如果不明白其转型规则,可能会出现让人匪夷所思的结果。
举例来说:
'' == false // true' ' == false // true'' == ' ' // false具体的转型规则如下:
- 两边是对象(引用类型),比较内存地址是否相同。
- 一边是原始类型,一边是对象,则将对象转换成原始类型再比较
- 两边都是原始类型:
- 3.1. 一边是 Symbol,则返回
false - 3.2. 两边类型一样,直接比较值
- 3.3. 一边为
undefined或null,则比较另一边是否也为undefined或null - 3.4. 一边为 “数字” 或者 “布尔值”,则以 “数字” 进行比较
看下面代码理解:
// 结果 false,两边都是原始类型,类型相同,非 Symbol,直接比较值'' == ' '
// 结果 true,两边都是原始类型,一边是布尔值,则以数字进行比较,两边都是 `0`false == ''
// 结果 true,两边都是原始类型,一边是布尔值,则以数字进行比较,两边都是 `0`false == ' '
// 结果 true。// 数组先转原始类型,尝试 valueOf 返回数组不采纳,// 继续 toString 返回空字符串,采纳,// 然后一边为布尔值,则以数字进行比较,// 空字符串和 false 都转换成 0,于是判断成立[] == false
// 结果 true。// 数组先转原始类型,尝试 valueOf 返回数组不采纳,// 继续 toString 返回空字符串,采纳,// 然后一边为数字,则以数字进行比较,// 空字符串转换成 0,于是判断成立[] == 0
// 结果 true。// 对象先转原始类型,尝试 valueOf,获得原始类型值,进行比较({ valueOf: function() { return false } }) == false({ valueOf: function() { return '' } }) == false({ valueOf: function() { return 0 } }) == false({ valueOf: function() { return 1 } }) == true({ valueOf: function() { return 2 } }) != true({ valueOf: function() { return 2 } }) != false由此可见,使用 == 进行比较,许多情况下,结果可以说是非常诡异的,稍不注意就可能出错。而且记忆这些转换规则的负担也很大。
所以,通常我们在制定代码规范的时候,都会增加一条总是使用 === 的规则来绕过避开这种复杂性。
尽管如此,双等号用于判断 null 和 undefined 的时候,还是比较简洁的:
// 使用 == null 可以同时处理 null 和 undefined 两种情况if (name == null) { // ...}
// 等价于if (name === null || name === undefined) { // ...}所以,如果真要用,个人推荐仅用于这种判断 null 的情况。
=== 三等号
使用三个 ===,可以强制让 JavaScript 不自动转型,进行严格比较:
'1' === 1 // false从而避开了上述 == 的一系列坑点。
但是,三等号比较也不总是完全符合人们的直觉,例如考虑下面的情况:
NaN === NaN // false还有:
-0 === 0 // true
// 但:;(1 / -0) === (1 / 0) // false所以,我们还需要其他比较算法来应付这种情况。
SameValueZero
SameValueZero 算法,其在 === 的基础上,增加了对 NaN 的特殊处理,NaN 和 NaN 之间在该算法中,是相等的。
该算法在语言中有众多应用,比如语言内置的 TypedArray(如 Float64Array 等等)、ArrayBuffer、Map、Set 等等。例如:
const set = new Set([NaN, NaN]) // Set(1) {NaN}console.log(set.size) // 1set.has(NaN) // true
const map = new Map([[NaN, NaN], [NaN, NaN]]) // Map(1) {NaN => NaN}console.log(map.size) // 1map.has(NaN) // true
const arr = new Float64Array([NaN]) // Float64Array [NaN]arr.includes(NaN) // true值得关注的是,ES7 中的 Array.prototype.includes 也使用了该比较算法,如下:
// 旧 ES 版本检查数组是否存在元素经常使用 indexOf,但无法处理 NaN[NaN].indexOf(NaN) !== -1 // false
// 而新版本的方案,使用 includes,可以完美支持 NaN[NaN].includes(NaN) // true了解了 SameValueZero 算法的特性后,我们就可以简单地写出实现了:
function sameValueZero(a, b) { if (a === b || (a !== a && b !== b)) return true return false}核心是利用了 NaN !== NaN 的奇葩性质,来特殊处理两个 NaN 的比较。
SameValue
以上的三种相等算法,都无法区分正负 0,SameValue 算法的行为,正是在 SameValueZero 的基础上, 再增加对正负 0 的区分处理。
JavaScript 中,内置有 SameValue 算法的实现:Object.is:
Object.is(0, 0) // trueObject.is(0, -0) // falseObject.is(NaN, NaN) // true当然,自己实现也并不麻烦:
const sameValue = typeof Object.is === 'function' ? Object.is : function(a, b) { if (typeof a === 'number') { // +0, -0 的情况 if (a === b) return 1 / a === 1 / b // a,b 均为 NaN 返回 true if (a !== a && b !== b) return true return false } return a === b }实现的核心就是利用正数除以 +0 和 -0,
会得到 Infinity 和 -Infinity 的性质,对 +-0 进行区分。
全文完