记住一句话:
一、概念
在 JavaScript 中我们经常会使用 ===(Strict equality,全等运算符)和 ==(Equality,相等运算符)来比较两个操作数是否相等,它俩都返回一个布尔值的结果(否定形式分别是 !== 和 !=)。
两者的区别:
-
===总是认为不同类型的操作数是不相等的。 -
==与前者不同,它会尝试强制类型转换且比较不同类型的操作数。(即如果操作数的类型不同,相等运算符会在比较之前尝试将它们转换为相同的类型)
放一张很经典的图,相信很多小伙伴都看过了。

二、全等运算符
全等运算符(===和 !==)使用全等比较算法来比较两个操作数。
从 MDN 可以看到,大致概括如下:
列举几个示例:
console.log(1 === '1') // false
console.log(1 === true) // false
console.log(undefined === null) // false
console.log({} === {}) // false
以上这些结果相信是毫无疑问的。但是,从写下这篇文章的时候 MDN 上关于 === 的描述并未涉及 Symbol、BigInt 类型。
那么,我们直接看 ECMAScript 最新标准吧。(#sec-7.2.16)

看起来“好像”只是简单的几句话对吧,翻译一下:
需要注意的是,Type(x) 是 ECMAScript 标准定义的抽象操作(Abstract Operation),并非是 JavaScript 的某个语法,它返回的是 8 种数据类型。如果你多读 ECMAScript 标准,就会发现很多抽象操作里都有引用到 Type(x),假设每处都对 Type(x) 进行描述显得多余重复,何不将它抽出来定义成一个抽象操作。(我猜 ECMAScript 标准修订者也是这么想的)
原来它是这样的三句话,哎~
1. Number::equal(x, y)

翻译过来就是:
1 / +0 // Infinity
1 / -0 // -Infinity
Infinity === -Infinity // false
2. BigInt::equal(x, y)

如果 x 和 y 均为 BigInt 类型,且具有相同的数学整数值(mathematical integer),则返回 true,否则返回 false。
3. SameValueNonNumeric(x, y)

翻译过来就是:
4. 总结一下
三、全等运算符的“坑”
根据以上的比较算法,感觉 === 也并不是总“靠谱”。例如以下“反直觉”的判断:
// PS:NaN 是一个全局对象的属性,与 Number.NaN 的值一样,都为 NaN(Not-A-Number)。
console.log(NaN === NaN) // false
console.log(Number.NaN === NaN) // false
console.log(+0 === -0) // true
1. NaN
因此,NaN !== NaN 结果为 true 似乎没毛病,只是反人类、反直觉罢了。
下面总结了几种方法,来判断一个值是否为 NaN。
// 1. 利用 NaN 的特性,JavaScript 中唯一一个不等于自身的值
function isnan(v) {
return v !== v
}
// 2. 利用 ES5 的 isNaN() 全局方法
function isnan(v) {
return typeof v === 'number' && isNaN(v)
}
// 3. 利用 ES6 的 Number.isNaN() 方法
function isnan(v) {
return Number.isNaN(v)
}
// 4. 利用 ES6 的 Object.is() 方法
function isnan(v) {
return Object.is(v, NaN)
}
因此,无法通过 Array.prototype.indexOf() 来确定 NaN 在数组中的索引值。
[NaN].indexOf(NaN) // -1
可使用 ES6 的 Arra.prototype.includes 方法判断
[NaN].includes(NaN) // true
2. +0 与 -0
在 JavaScript 中,数字类型包括浮点数、+Infinity(正无穷)、-Infinity(负无穷)和 NaN(not-a-number,非数字)。
还有 ES2021 标准中增加了一种 BigInt(原始)类型,表示极大的数字(非本文范围,不展开叙述)。
其实,+0 与 -0 是不相等的,为什么?
console.log(1 / +0 === Infinity) // true
console.log(1 / -0 === -Infinity) // true
console.log(Infinity === -Infinity) // false
// 因此,+0 和 -0 是两个不相等的值。只是“全等比较算法”里认为他们是相对的而已。
3. 处理以上两种特殊的情况
在 ES6 标准中,提出来一个方法 Object.is(),对以上两种情况都做了“正确”的处理。
Object.is(NaN, NaN) // true
Object.is(+0, -0) // false
而 ES5 可以这样去处理:
function objectIs(x, y) {
if (x === y) {
// x === 0 => compare via infinity trick
return x !== 0 || (1 / x === 1 / y)
}
// x !== y => return true only if both x and y are NaN
return x !== x && y !== y
}
四、相等运算符
相等运算符(== 和 !=)使用抽象相等比较算法比较两个操作数。
1. ES5 相等比较算法
在 MDN 可以看到 x == y 比较的描述,如下:
截止发文日期,我们可以看到它并没有关于 Symbol 和 BigInt 类型的描述,因此以上相等比较并不是最新的。
2. ES6+ 相等比较算法
目前 ECMAScript 标准最新的抽象相等比较算法如下:

翻译一下:
相等运算符与全等运算符(===)运算符之间最显着的区别在于,后者不尝试类型转换。相反,前者始终将不同类型的操作数视为不同。
列举几个示例:
// 这些例子都是很简单的
console.log(false == undefined) // false
console.log(null == undefined) // true
console.log(null == false) // false
console.log(null == 0) // false
console.log(null == '') // false
console.log('\n 123 \t' == 123) // true, because conversion to number ignores leading and trailing whitespace in JavaScript.
相信很多人在没有完全弄清楚相等运算符比较的【套路】之前,会很让人抓狂...
针对以上 13 条规则,再提炼总结一下(但是标准描述真的是非常地严谨 ?):
- 如果两个操作数是同一类型,返回
x === y的结果。 - 如果一个操作数是
null,另一个操作数是undefined,则返回true。 - 如果一个操作数是 Number 类型,另一个是操作数 String 类型,那么会将 String 类型的操作数先转换为 Number 类型,接着按第 1 点去比较并返回结果。
- 如果一个操作数
x是 BigInt 类型,另一个是操作数y是 String 类型,那么会将 String 类型的操作数先转换为 BigInt 类型(假设转换结果为n)。- 如果
n为NaN,则返回false。 - 否则返回
x == n的结果。
- 如果
- 如果一个操作数是 Boolean 类型,则会将布尔值转换为 Number 类型,并返回
x == ToNumber(y)的结果。 - 如果一个操作是引用类型,另一个是原始类型,那么会将引用值转换为原始值,并返回
ToPrimitive(x) == y。 - 如果一个操作数是 BigInt 类型,另一个是 Number 类型,那么
- 如果其中一个数是
NaN、Infinity或-Infinity中的任何一个,则返回false。 - 如果
x和y的数学值相等,则返回true,否则返回false。
- 如果其中一个数是
- 返回
false(包括以上转换过程出现TypeError都会在这里返回false)。
五、相等比较常见示例
其实熟悉以上算法之后,遇到一些看似很“奇葩”的比较也不足为惧了。
1. 0 == false
0 == null // false
0 == undefined // false,同理
根据 Type(x) 抽象操作规则,Type(0) 结果为 Number,Type(null) 结果为 Null,再结合比较算法,可以清楚地知道它按照第 13 点返回 false。
2. [] == ![]
看到这个式子,千万别急眼,一步一步来......
[] == ![] // true
我们来分析一下,根据运算符优先级,! (逻辑非)的优先级高于 ==,因此优先执行 ![]。而 [] 属于真值(Truthy) ,所以 ![] 结果为 false。
[] == false
根据第 9 条规则,先将 Boolean 类型的值转换为 Number 类型,所以变成了:
[] == 0
再根据第 11 条规则,先将引用类型转换为原始类型,故进行的操作是 ToPrimitive([])。
(这步操作可能稍微复杂一点点,但别急)由于数组实例本身没有 @@toPrimitive 方法,且此时 ToPrimitive 操作的 hint 值为 "default"(根据 ToPrimitive 规则,里面的其中一个步骤会将 hint 设为 "number"),然后进行 OrdinaryToPrimitive 操作,因此会先调用 valueOf 方法,再调用 toString 方法。
// 由于
[].valueOf() // 结果为数组本身,并非原始值,接着调用 toString 方法
[].toString() // 结果为 "",所以 [] 的 ToPrimitive 操作返回的是空字符串
// 所以变成了
'' == 0
根据第 5 条规则,将 "" 转换为数值 0,即
0 == 0 // true
严格来说,其实还有一步的。根据第 1 条规则,返回 0 === 0 的结果。
整个转换过程如下,因此 [] == ![] 比较结果为 true。
[] == ![]
[] == false
[] == 0
'' == 0
0 == 0
0 === 0 // true
3. {} == !{}
它看着跟前面一个例子很相似,转换过程同理,但结果是...
{} == !{} // false
首先,根据操作符优先级顺序,先将 !{} 转换为 false。即变成了 {} == false 的比较,然后将 false 转换为 0,所以变成了。
{} == 0
然后将 {} 转换为原始值,即执行 ToPrimitive({}) 操作。其中 hint 为 "default"。所以先执行 {}.toString() 方法,得到 "[object Object]" 结果,由于结果已经是原始值,不再调用 valueOf() 方法了。
"[object Object]" == 0
根据第 5 条规则,将 "[object Object]" 转换为数值,进行的操作是 ToNumber("[object Object]"),即执行方法 Number("[object Object]"),得到的结果是 NaN。
NaN == 0
由于 NaN、0 都是 Number 类型,根据第 1 条规则,返回 NaN === 0 的结果。而 NaN 与任何操作数(包括其本身)进行全等比较,均返回 false。
因此 {} == !{} 的结果为 false。整个过程如下:
{} == !{}
{} == false
{} == 0
'[object Object]' == 0
NaN == 0
NaN === 0 // false
4. 10 == 10n
来看看 Number 类型与 BigInt 类型的相等比较。
10 == 10n // true
根据规则第 12 条,且两个操作数并非是 NaN、Infinity、-Infinity,因此比较两者的数学值(mathematical value),其数学值是相等的,所以结果为 true。
五、其他
1. +、-、*、/、% 的隐式类型转换
除了 +、- 既可以作为一元运算符、也可以是算术运算符,其余的均为算术运算符。
当
+、-、*、/、%均为算术运算符时,会将运算符两边的操作数先转换为 Number 类型(若已经是 Number 类型则无需再转换),再进行相应的算术运算。当
+、-作为一元运算符时,即只有一个运算符和操作数。前者将操作数转换为 Number 类型并返回。后者将操作数转换为 Number 类型,然后取反再返回。
未完待续,拼命更新中...










