先记住这个答案
[] == ![] 要先算右边。! 会把操作数转成布尔值,空数组是对象,对象都是真值,所以 ![] 是 false,式子变成 [] == false。宽松相等遇到布尔值时会先把它转成数字,得到 [] == 0。一边是对象、一边是数字,对象要转成原始值,空数组的 toString() 是 '',得到 '' == 0。字符串再转数字,'' 是 0,最后 0 == 0 为 true。[] == [] 就简单了,两边都是对象时 == 不做任何转换,只比较是不是同一个对象,两个 [] 字面量各自创建了一个新数组,所以是 false。
- 空数组是真值,
![]是false ==遇到布尔值,先把布尔值转成数字- 对象和数字比较时,对象先转原始值
- 两个对象比较只看引用,不做任何转换
一步步拆开
把每一步的中间结果都打印出来,推导过程就一目了然。
console.log(![]) // 第一步
console.log([] == false, [] == 0) // 第二、三步
console.log(JSON.stringify(String([])), '' == 0) // 数组转原始值、字符串转数字
console.log([] == ![], [] == [])
const a = []
console.log(a == a, a == []) // 同一个引用才相等
console.log({} == !{}) // 对象换成 {}
console.log([0] == false, [1] == true, [2] == true)查看输出与解释
false
true true
"" true
true false
true false
false
true true false第三行的 "" 就是空数组转出来的空字符串。最后两行是面试常见的变体。{} == !{} 为 false,因为 !{} 是 false,转数字得 0;而 {} 转原始值是 '[object Object]',转数字是 NaN,NaN 不等于任何值。[2] == true 为 false,因为 true 先变成 1,[2] 变成 '2' 再变成 2,2 == 1 不成立。
宽松相等的转换优先级
== 的规则看起来多,其实可以按优先级记成几句话。两边类型相同,直接比较,对象就比引用。null 和 undefined 只和彼此相等。有布尔值,先把布尔值转数字。数字和字符串,字符串转数字。对象和原始值,对象转原始值(先试 valueOf,拿不到原始值再用 toString)。转完之后类型还不一样,就回到上面继续比。
对照这个顺序看 [] == ![],每一步只用到其中一条规则。关键是意识到 ! 是单独的运算,它用 ToBoolean,而 == 本身从不把对象转成布尔值。
容易答错的地方
- 以为
==会把数组转成布尔值再比较 - 如果是这样,
[]应该转成true,[] == false就该是false。实际上==遇到布尔值是把布尔值转数字,对象走的是转原始值的路线。 - 以为
[] == []会比较内容 ==和===在两边都是对象时行为一样,比较的都是引用。想比较数组内容要自己逐项比,或者用 lodash 的isEqual,不能用JSON.stringify一把梭,它会忽略undefined、把NaN变成null。- 漏掉
!的优先级 [] == ![]里!优先级高于==,先算的是![],而不是!([] == [])。
面试官还会怎么问?
怎么让 a == 1 && a == 2 && a == 3 成立?
利用对象转原始值时会调用 valueOf:定义 const a = { n: 1, valueOf() { return this.n++ } },每次 == 比较都会调用一次,依次返回 1、2、3。对象和数字比较时,先把对象转成原始值,再和数字比较。
[] + [] 和 [] == '' 分别是什么?
[] + [] 是空字符串,两个数组都转成 '' 再拼接。[] == '' 是 true,数组转原始值就是 ''。
项目里还有必要记这些规则吗?
写代码时统一用 ===,配合 ESLint 的 eqeqeq 规则,基本碰不到这些情况。记规则的意义在于读老代码、排查奇怪的比较结果时能快速推出原因。唯一常见的例外是 value == null,它能同时判断 null 和 undefined。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。