第 7 章

抽象相等比较:== 的12条规则完整推导

[] == ![] 在 JavaScript 中为 true。这不是 bug,而是严格遵循规范中 12 条相等规则逐步推导的必然结果。

🔹 Level 1 · 你需要知道的

何时用 == 是安全的

只有一种情况推荐使用 ==检查 null 或 undefined

// ✅ 唯一推荐的 == 使用场景:同时检查 null 和 undefined
if (value == null) {
  // value 是 null 或 undefined 时进入
}

// 等价于(但更冗长):
if (value === null || value === undefined) {
  // 同上
}

// 实际业务示例
function getUser(id) {
  const user = db.find(id);
  if (user == null) {       // 无论 find 返回 null 还是 undefined,都安全
    throw new Error("User not found");
  }
  return user;
}

绝对不能用 == 的情况

// ❌ 与 0 比较(false、""、null、undefined 都 == 0 吗?不完全是,但容易误判)
0 == false     // true (boolean 转 number:false → 0)
0 == ""        // true ("" 转 number:0)
0 == null      // false(null 只等于 undefined)
0 == undefined // false(同上)
0 == "0"       // true ("0" 转 number:0)

// ❌ 与 "" 比较
"" == false    // true (boolean 先转 number:false → 0,"" 转 number → 0)
"" == 0        // true
"" == null     // false

// ❌ 与 false 比较(最危险,因为很多值都 == false)
false == 0      // true
false == ""     // true
false == "0"    // true
false == null   // false
false == []     // true  ← 震惊!
false == {}     // false ← 同样令人困惑

// ❌ 任何对象与原始值比较(会经过 ToPrimitive)
[] == ""       // true([] → "" → 相等)
[] == 0        // true([] → "" → 0 → 相等)
[1] == 1       // true([1] → "1" → 1 → 相等)

ESLint eqeqeq 规则的原因

eqeqeq 规则(强制使用 ===)的存在理由:

  1. 可读性:阅读代码时,=== 的语义明确,== 的语义需要熟记12条规则。
  2. 防御性编程== 的类型转换在函数接收意外类型时会掩盖 bug。
  3. 维护成本:使用 == 的代码在重构时容易引入隐蔽 bug。
// ESLint 规则配置
{
  "rules": {
    "eqeqeq": ["error", "always", { "null": "ignore" }]
    // "null": "ignore" 允许 x == null 这种合法用法
  }
}

🔸 Level 2 · 它是怎么运行的

Abstract Equality Comparison 决策树

IsLooselyEqual(x, y)
│
├─ x 和 y 类型相同?
│   └─ YES → 使用严格相等(IsStrictlyEqual)直接比较
│             (包括 NaN !== NaN,+0 === -0 等严格相等的规则)
│
└─ NO(类型不同):
    │
    ├─ x 是 null,y 是 undefined? → true(规范硬编码)
    ├─ x 是 undefined,y 是 null? → true(规范硬编码)
    │
    ├─ x 或 y 是 Number,另一个是 String?
    │   └─ 将 String 转为 Number,递归调用 IsLooselyEqual
    │       "42" == 42  →  42 == 42  →  true
    │       "abc" == 1  →  NaN == 1  →  false
    │
    ├─ x 或 y 是 BigInt,另一个是 String?
    │   └─ 尝试将 String 转为 BigInt,失败则返回 false
    │
    ├─ x 或 y 是 Boolean?
    │   └─ 将 Boolean 转为 Number(true→1, false→0),递归调用
    │       ⚠️ 这是最大的陷阱!boolean 先变数字,不是直接与另一个操作数比较真假
    │       false == ""  →  0 == ""  →  0 == 0  →  true
    │
    ├─ x 或 y 是 Object,另一个是 String/Number/BigInt/Symbol?
    │   └─ 对 Object 调用 ToPrimitive,递归调用 IsLooselyEqual
    │       [] == ""  →  "" == ""  →  true
    │       [] == 0   →  "" == 0   →  0 == 0  →  true
    │
    ├─ x 是 BigInt,y 是 Number(或反之)?
    │   └─ 比较数学值(有限数值才相等,NaN/Infinity 特殊处理)
    │       1n == 1   →  true
    │       1n == 1.5 →  false
    │
    └─ 其他所有情况 → false

boolean 参与比较的陷阱图解

false == "0" 的推导(最易混淆的案例):

第1步:检测到 boolean
  false == "0"
  └─ 规则:boolean → 转 number
  → 0 == "0"

第2步:检测到 number 和 string
  0 == "0"
  └─ 规则:string → 转 number
  → 0 == 0

第3步:同类型,用严格相等
  0 === 0 → true

总结:false == "0" 为 true!

初学者的直觉错误:
  "0" 是非空字符串,直觉认为它是 truthy,truthy != false
  但 == 不检查 truthy/falsy!它做类型转换后数值比较。

🔺 Level 3 · 规范怎么定义的

7.2.13 IsLooselyEqual(x, y) 完整12条规则

规范原文(ECMA-262 第 7.2.13 节):

7.2.13 IsLooselyEqual ( x, y )

  1. If Type(x) is Type(y), then a. Return IsStrictlyEqual(x, y).
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. NOTE: This step is replaced in section B.3.6.2.
  5. If x is a Number and y is a String, return ! IsLooselyEqual(x, ! ToNumber(y)).
  6. If x is a String and y is a Number, return ! IsLooselyEqual(! ToNumber(x), y).
  7. If x is a BigInt and y is a String, then a. Let n be StringToBigInt(y). b. If n is undefined, return false. c. Return ! IsLooselyEqual(x, n).
  8. If x is a String and y is a BigInt, return ! IsLooselyEqual(y, x).
  9. If x is a Boolean, return ! IsLooselyEqual(! ToNumber(x), y).
  10. If y is a Boolean, return ! IsLooselyEqual(x, ! ToNumber(y)).
  11. If x is either a String, a Number, a BigInt, or a Symbol and y is an Object, return ! IsLooselyEqual(x, ? ToPrimitive(y)).
  12. If x is an Object and y is either a String, a Number, a BigInt, or a Symbol, return ! IsLooselyEqual(? ToPrimitive(x), y).
  13. If x is a BigInt and y is a Number, or if x is a Number and y is a BigInt, then a. If x or y is NaN, +∞𝔽, or -∞𝔽, return false. b. If ℝ(x) = ℝ(y), return true; otherwise return false.
  14. Return false.

逐条中文解读

规则 触发条件 操作
1 两侧类型相同 直接走严格相等(含 NaN!==NaN)
2 x=null, y=undefined 硬编码返回 true
3 x=undefined, y=null 硬编码返回 true
5 x=Number, y=String y 转 Number,递归
6 x=String, y=Number x 转 Number,递归
7-8 BigInt 和 String String 转 BigInt,转换失败返回 false
9 x=Boolean x 转 Number,递归(注意:不是 y 转
10 y=Boolean y 转 Number,递归
11 y=Object,x 是原始值 y 走 ToPrimitive,递归
12 x=Object,y 是原始值 x 走 ToPrimitive,递归
13 BigInt 和 Number 按数学值比较
14 其他 false

规范中的 ! 符号:在规范算法中,! 前缀表示"断言此操作不会产生 abrupt completion(异常)",不是 JavaScript 的逻辑非运算符。


💎 Level 4 · 边界与陷阱

陷阱1:'' == false 为 true 的5步推导

表达式:'' == false

类型:String == Boolean

步骤1:规则10:y(false)是 Boolean
  → ToNumber(false) = 0
  → 递归:'' == 0

类型:String == Number

步骤2:规则6:x('')是 String,y(0)是 Number
  → ToNumber('') = 0
  → 递归:0 == 0

类型:Number == Number

步骤3:规则1:两侧类型相同(都是 Number)
  → IsStrictlyEqual(0, 0)
  → 0 === 0 → true

结论:'' == false 为 true

直觉为什么会错:
  '' 是 falsy,false 是 falsy,感觉应该相等
  但 == 不比较 truthy/falsy,而是做数值化比较

陷阱2:[] == false 为 true 的6步推导

表达式:[] == false

类型:Object == Boolean

步骤1:规则10:y(false)是 Boolean
  → ToNumber(false) = 0
  → 递归:[] == 0

类型:Object == Number

步骤2:规则12:x([])是 Object,y(0)是 Number
  → ToPrimitive([]) 
  → [].valueOf() → [](对象,继续)
  → [].toString() → ""(原始值,返回)
  → 递归:"" == 0

类型:String == Number

步骤3:规则6:x("")是 String,y(0)是 Number
  → ToNumber("") = 0
  → 递归:0 == 0

类型:Number == Number

步骤4:规则1:类型相同
  → 0 === 0 → true

结论:[] == false 为 true
注意:经历了两次类型转换(Object→String→Number)

陷阱3:[] == ![] 为 true 的完整推导

这是 JavaScript 中最著名的"诡异"表达式:

表达式:[] == ![]

步骤1:先求 ![] 的值(一元运算符,优先级高于 ==)
  ![] = !ToBoolean([])
  ToBoolean([]) = true(所有对象都是 truthy,包括空数组)
  !true = false
  所以 ![] = false

现在表达式变为:[] == false

步骤2:[] == false 的推导(同上陷阱2)
  → [] == 0(boolean 转 number)
  → "" == 0(ToPrimitive)
  → 0 == 0(string 转 number)
  → true

结论:[] == ![] 为 true

根本原因:
  [] 是 truthy(作为 boolean 是 true)
  ![] 是 false(对 truthy 取反)
  但 [] == false 走的是数值比较路径,[] 转数字为 0,false 转数字为 0,相等

陷阱4:null == 0 为 false 的原因

许多开发者误以为 null 会参与数值比较,实际上 null 有专属规则:

表达式:null == 0

类型:Null == Number

逐条检查规则:
  规则1:类型不同(Null != Number)→ 继续
  规则2:x=null,y=undefined? → y=0 不是 undefined → 继续
  规则3:x=undefined? → x=null 不是 undefined → 继续
  规则5:x=Number,y=String? → 否 → 继续
  规则6:x=String,y=Number? → 否 → 继续
  规则9:x=Boolean? → 否 → 继续
  规则10:y=Boolean? → 否 → 继续
  规则11:y=Object? → y=0 不是对象 → 继续
  规则12:x=Object? → x=null 不是对象 → 继续
  规则13:BigInt? → 否 → 继续
  规则14:return false

结论:null == 0 为 false!

null 只等于 undefined(规则2、3),不等于任何其他值,
  包括 0、""、false 这些假值。
// null 的相等关系总结
null == null        // true(规则1,类型相同,严格相等)
null == undefined   // true(规则2,硬编码)
null == 0           // false
null == ""          // false
null == false       // false
null == "null"      // false
null == NaN         // false
null == []          // false(即使 [] 是 falsy 值)

陷阱5:== 传递性失败

数学上相等关系要满足传递性:如果 a=b 且 b=c,则 a=c。== 不满足这个性质:

"0" == 0    // true ("0" 转 number:0,0 == 0)
0 == ""     // true ("" 转 number:0,0 == 0)
"0" == ""   // false!(类型相同都是 string,"0" !== "")

// 更完整的三角失败例子:
false == 0      // true
false == ""     // true
0 == ""         // true
// 看起来都相等,但 false、0、"" 三者互相相等,是"闭环",没有传递性问题

// 传递性真正失败的案例:
"1" == 1    // true("1" → 1 → true)
1 == true   // true(true → 1 → true)
"1" == true // true(true → 1,"1" → 1 → true)
// 这三个是自洽的,没问题

// 失败案例:
"0" == 0    // true
"0" == ""   // false
// 但 0 == ""  // true
// 因此 "0" 和 "" 都 == 0,但 "0" != ""
// 这就是传递性的失败:a==b, b==c 但 a!=c
// 其中 a="0", b=0, c=""

验证代码

console.log("0" == 0);   // true  ← "0" 转 number
console.log(0  == "");   // true  ← "" 转 number
console.log("0" == "");  // false ← 同为 string,直接比较

// 传递性失败!"0" == 0 且 0 == "" 但 "0" != ""

这正是 ESLint eqeqeq 规则的核心理由:== 不满足数学意义上的等价关系。

陷阱6:== 的性能开销

// 微基准测试(Chrome 120,单位:毫秒/百万次操作)
// === : ~1ms(无类型转换,直接位比较)
// == 同类型: ~1ms(实际走 === 路径,无差异)
// == 跨类型: ~3-8ms(视转换复杂度而定)

// 对于高频执行的代码(如渲染循环),使用 === 而非 ==
// 不是因为 === "更安全",而是因为它的语义直观且性能更稳定

本章小结

  1. == 的唯一推荐用法是 x == null:利用规则2/3的硬编码,同时捕获 null 和 undefined,这是一个被规范明确支持的特性,配合 ESLint 的 { "null": "ignore" } 选项。

  2. boolean 参与 == 比较时最危险:规则9/10 将 boolean 先转为数字(true→1, false→0),而不是将另一个操作数转为布尔值。这导致 false == "" 为 true,但 false == "false" 为 false。

  3. null 只等于 undefined:null 不走任何类型转换路径(规则5-13都不匹配),只有规则2/3(与 undefined 的硬编码互等)会让它为 true。null == 0null == "" 都是 false。

  4. [] == ![] 为 true 的原因![] 先求值为 false(空数组是 truthy),然后 [] == false 按规则经过两次转换最终比较 0 == 0。每一步都是规范的必然结果。

  5. == 不满足传递性"0" == 0(true)且 0 == ""(true),但 "0" != ""(false)。这违反了数学等价关系的传递律,是 eqeqeq 规则存在的根本原因,而非仅仅是"编码风格"问题。

本章评分
4.6  / 5  (53 评分)

💬 留言讨论