JavaScript 中的抽象相等(==)转义算法:处理 null/undefined 与原始类型的全量递归逻辑

在JavaScript的世界里,相等性比较是一个看似简单实则深奥的话题。我们日常使用最多的莫过于 ==(抽象相等)和 ===(严格相等)这两个操作符。其中,=== 的行为相对直观:它要求被比较值的类型和值都必须相同。然而,== 的行为则要复杂得多,因为它引入了“类型转换”(Type Coercion)的概念,即在比较之前尝试将操作数转换为相同的类型。这种转换过程,尤其是处理 null/undefined 与原始类型以及对象到原始类型的全量递归逻辑,正是我们今天深入探讨的核心。我们将它称为“抽象相等转义算法”,因为它描述了值如何“逃离”其原始类型以寻求可比较的形式。

我们将以ECMAScript规范(通常是ECMA-262)为蓝本,详细剖析这个算法的每一个步骤,理解其内在机制,并通过丰富的代码示例来验证和巩固我们的理解。


1. 抽象相等比较算法(==)的核心原理

抽象相等比较算法的目的是确定两个操作数 xy 是否“相等”,即使它们的类型不同。它的核心思想是:如果两个操作数可以被合理地转换为相同的类型,并且转换后的值相等,那么它们就是抽象相等的。这个转换过程并非随意的,而是遵循一套严格的、预定义的规则。

在深入细节之前,我们先概括一下算法的几个关键原则:

  1. 同类型比较:如果 xy 的类型相同,那么比较就非常直接,类似于 ===(但有一些细微差别,比如 NaN+0/-0)。
  2. nullundefined 特殊性null 只与 undefined 相等,不与任何其他值(包括 0false)相等。undefined 也只与 null 相等。这是算法中的一个明确的“逃逸”规则。
  3. 原始类型向数字转换偏好:当一个操作数是数字,另一个是字符串或布尔值时,非数字的操作数通常会被尝试转换为数字。
  4. 对象向原始类型转换:如果一个操作数是对象,另一个是原始类型,那么对象会被尝试转换为一个原始类型。这个过程可能涉及 valueOf()toString() 方法。
  5. 不进行比较的情况:有些类型之间根本无法进行有意义的转换,或者转换会导致 TypeError,此时算法会直接返回 false。例如,Symbol 类型与其他类型(除了自身)进行 == 比较时,通常不会发生有意义的类型转换。

理解这些原则是掌握 == 行为的基础。现在,让我们一步步地解构ECMAScript规范中定义的抽象相等比较算法。


2. 抽象相等比较算法的详细步骤(x == y

以下是ECMAScript规范中定义 x == y 算法的简化但逻辑严谨的流程。我们将 Type(x)Type(y) 表示操作数的内部类型(如 Undefined, Null, Number, String, Boolean, Symbol, BigInt, Object)。

算法:Abstract Equality Comparison (x, y)

  1. If Type(x) is Type(y):

    • If x is Undefined, return true.
      • undefined == undefined // true
    • If x is Null, return true.
      • null == null // true
    • If x is Number:
      • If x is NaN, return false.
      • If y is NaN, return false.
      • If x is the same Number value as y, return true.
      • Return false.
      • 5 == 5 // true
      • NaN == NaN // false (这是与 === 相同的重要行为)
      • +0 == -0 // true (这也是与 === 相同的重要行为)
    • If x is String:
      • If x and y are exactly the same sequence of code units, return true.
      • Return false.
      • "hello" == "hello" // true
    • If x is Boolean:
      • If x and y are both true or both false, return true.
      • Return false.
      • true == true // true
    • If x is Symbol:
      • If x and y are the same Symbol value, return true.
      • Return false.
      • Symbol('a') == Symbol('a') // false (Symbol是唯一的)
      • let s = Symbol('a'); s == s // true (引用相同)
    • If x is BigInt:
      • If x is the same BigInt value as y, return true.
      • Return false.
      • 10n == 10n // true
    • If x is Object:
      • If x and y are the same Object value (i.e., refer to the same object in memory), return true.
      • Return false.
      • let a = {}; a == a // true
      • {} == {} // false (不同引用)
  2. If x is null and y is undefined, return true.

    • null == undefined // true
  3. If x is undefined and y is null, return true.

    • undefined == null // true
  4. If Type(x) is Number and Type(y) is String:

    • Return x == ToNumber(y).递归调用
    • 这里,y (String) 会被转换为数字。
    • 5 == '5' 实际上是 5 == ToNumber('5') -> 5 == 5 -> true
    • 0 == '' 实际上是 0 == ToNumber('') -> 0 == 0 -> true
    • 1 == '0x1' 实际上是 1 == ToNumber('0x1') -> 1 == 1 -> true
  5. If Type(x) is String and Type(y) is Number:

    • Return ToNumber(x) == y.递归调用
    • 这是上一步的对称情况。
    • '5' == 5 实际上是 ToNumber('5') == 5 -> 5 == 5 -> true
  6. If Type(x) is BigInt and Type(y) is String:

    • Let n be ToBigInt(y). If n is an abrupt completion (e.g., SyntaxError), return false.
    • Return x == n.递归调用
    • 5n == '5' 实际上是 5n == ToBigInt('5') -> 5n == 5n -> true
    • 5n == 'abc' 实际上是 5n == ToBigInt('abc') (throws SyntaxError) -> false
  7. If Type(x) is String and Type(y) is BigInt:

    • Let n be ToBigInt(x). If n is an abrupt completion, return false.
    • Return n == y.递归调用
    • 这是上一步的对称情况。
    • '5' == 5n 实际上是 ToBigInt('5') == 5n -> 5n == 5n -> true
  8. If Type(x) is Boolean:

    • Return ToNumber(x) == y.递归调用
    • true 转换为 1false 转换为 0
    • true == '1' 实际上是 ToNumber(true) == '1' -> 1 == '1' -> (根据步骤4) 1 == ToNumber('1') -> 1 == 1 -> true
    • false == 0 实际上是 ToNumber(false) == 0 -> 0 == 0 -> true
    • false == '' 实际上是 ToNumber(false) == '' -> 0 == '' -> (根据步骤4) 0 == ToNumber('') -> 0 == 0 -> true
  9. If Type(y) is Boolean:

    • Return x == ToNumber(y).递归调用
    • 这是上一步的对称情况。
    • '1' == true 实际上是 '1' == ToNumber(true) -> '1' == 1 -> (根据步骤5) ToNumber('1') == 1 -> 1 == 1 -> true
  10. If Type(x) is Symbol and Type(y) is Number or String or BigInt:

    • Return false.
    • Symbol('a') == 1 // false
    • Symbol('a') == 'a' // false
  11. If Type(x) is Number or String or BigInt and Type(y) is Symbol:

    • Return false.
    • 1 == Symbol('a') // false
  12. If Type(x) is BigInt and Type(y) is Number:

    • If y is NaN, return false.
    • If y is +Infinity or -Infinity, return false.
    • If the mathematical value of x is equal to the mathematical value of y, return true.
    • Return false.
    • 10n == 10 // true
    • 10n == 10.0 // true
    • 10n == 10.1 // false
    • 10n == NaN // false
    • 10n == Infinity // false
  13. If Type(x) is Number and Type(y) is BigInt:

    • 这是上一步的对称情况。
    • 10 == 10n // true
  14. If Type(x) is Object and Type(y) is String or Number or Symbol or BigInt:

    • Return ToPrimitive(x) == y.递归调用
    • 对象 x 会被尝试转换为一个原始类型。
    • [5] == '5' 实际上是 ToPrimitive([5]) == '5' -> '5' == '5' -> true
    • [5] == 5 实际上是 ToPrimitive([5]) == 5 -> '5' == 5 -> (根据步骤5) ToNumber('5') == 5 -> 5 == 5 -> true
    • new Date(0) == 0 实际上是 ToPrimitive(new Date(0)) == 0 -> 0 == 0 -> true (Date对象默认 ToPrimitive 为 "number" 提示)
  15. If Type(x) is String or Number or Symbol or BigInt and Type(y) is Object:

    • Return x == ToPrimitive(y).递归调用
    • 这是上一步的对称情况。
    • '5' == [5] 实际上是 '5' == ToPrimitive([5]) -> '5' == '5' -> true
  16. Return false. (如果以上所有规则都不适用)

    • [] == {} // false (两者都转换为原始类型后,得到 ''"[object Object]", 然后 '' == "[object Object]" 为 false)
    • {} == [] // false
    • Symbol('a') == {} // false (根据步骤10/11的泛化)

3. 深入理解辅助操作:ToNumber, ToBigInt, ToPrimitive

抽象相等算法的递归性质严重依赖于几个内部抽象操作:ToNumberToBigIntToPrimitive。理解这些操作是掌握 == 行为的关键。

3.1 ToNumber(argument)

ToNumber 操作将一个值转换为一个数字。这是在 == 比较中最常见的类型转换之一。

输入类型 结果 示例
Undefined NaN ToNumber(undefined) -> NaN
Null +0 ToNumber(null) -> 0
Boolean 1 (如果是 true),0 (如果是 false) ToNumber(true) -> 1, ToNumber(false) -> 0
Number 返回自身 ToNumber(5) -> 5
String 解析字符串为数字。空字符串或只含空格的字符串为 0。无法解析的字符串为 NaN。支持十进制、十六进制等。 ToNumber("123") -> 123, ToNumber(" 123 ") -> 123, ToNumber("") -> 0, ToNumber("abc") -> NaN, ToNumber("0x10") -> 16
Symbol 抛出 TypeError ToNumber(Symbol('a')) -> TypeError
BigInt 抛出 TypeError (注意:==BigIntNumber 有特殊处理,不会直接通过 ToNumber 转换 BigInt) ToNumber(10n) -> TypeError
Object 首先调用 ToPrimitive(argument, hint: "number"),然后对结果调用 ToNumber ToNumber([]) -> ToNumber(ToPrimitive([], "number")) -> ToNumber('') -> 0
ToNumber([5]) -> ToNumber(ToPrimitive([5], "number")) -> ToNumber('5') -> 5
ToNumber({}) -> ToNumber(ToPrimitive({}, "number")) -> ToNumber('[object Object]') -> NaN

代码示例:

console.log(Number(undefined)); // NaN
console.log(Number(null));      // 0
console.log(Number(true));      // 1
console.log(Number(false));     // 0
console.log(Number("123"));     // 123
console.log(Number("  -42.5  "));// -42.5
console.log(Number(""));        // 0
console.log(Number("   "));     // 0
console.log(Number("abc"));     // NaN
console.log(Number("0xAF"));    // 175
console.log(Number(Symbol('test'))); // TypeError
// console.log(Number(10n));    // TypeError (注释掉,避免脚本中断)
console.log(Number([]));       // 0 (ToPrimitive([]) -> "")
console.log(Number([1]));      // 1 (ToPrimitive([1]) -> "1")
console.log(Number([1, 2]));   // NaN (ToPrimitive([1,2]) -> "1,2")
console.log(Number({}));       // NaN (ToPrimitive({}) -> "[object Object]")

3.2 ToBigInt(argument)

ToBigInt 操作将一个值转换为一个 BigInt

输入类型 结果 示例
Undefined 抛出 TypeError BigInt(undefined) -> TypeError
Null 抛出 TypeError BigInt(null) -> TypeError
Boolean 1n (如果是 true),0n (如果是 false) BigInt(true) -> 1n, BigInt(false) -> 0n
Number 抛出 TypeError (这是 BigIntNumber 之间不能直接转换的重要原因) BigInt(5) -> TypeError
String 解析字符串为 BigInt。字符串必须是有效的整数表示,不能有小数或非数字字符。空字符串或只含空格的字符串抛出 SyntaxError BigInt("123") -> 123n, BigInt(" -42 ") -> -42n, BigInt("") -> SyntaxError, BigInt("12.3") -> SyntaxError
Symbol 抛出 TypeError BigInt(Symbol('a')) -> TypeError
BigInt 返回自身 BigInt(10n) -> 10n
Object 首先调用 ToPrimitive(argument, hint: "number"),然后对结果调用 ToBigInt BigInt([]) -> BigInt(ToPrimitive([], "number")) -> BigInt('') -> SyntaxError
BigInt([5]) -> BigInt(ToPrimitive([5], "number")) -> BigInt('5') -> 5n

代码示例:

// console.log(BigInt(undefined)); // TypeError
// console.log(BigInt(null));      // TypeError
console.log(BigInt(true));      // 1n
console.log(BigInt(false));     // 0n
// console.log(BigInt(5));         // TypeError
console.log(BigInt("123"));     // 123n
console.log(BigInt("  -42  ")); // -42n
// console.log(BigInt(""));        // SyntaxError
// console.log(BigInt("12.3"));    // SyntaxError
// console.log(BigInt(Symbol('test'))); // TypeError
console.log(BigInt(10n));       // 10n
// console.log(BigInt([]));        // SyntaxError (ToPrimitive([]) -> "")
console.log(BigInt([5]));       // 5n (ToPrimitive([5]) -> "5")
// console.log(BigInt([1, 2]));    // SyntaxError (ToPrimitive([1,2]) -> "1,2")
// console.log(BigInt({}));        // SyntaxError (ToPrimitive({}) -> "[object Object]")

3.3 ToPrimitive(input, preferredType?)

ToPrimitive 操作将一个对象转换为一个原始值(string, number, boolean, symbol, null, undefined, bigint)。这是 == 算法中处理对象操作数的核心。preferredType 参数是一个可选的提示,可以是 "number""string",用于指导转换过程。如果没有提供 preferredType,则默认为 "number"(除非是 Date 对象,它默认为 "string")。

算法步骤:

  1. 如果 input 已经是原始值,直接返回 input
  2. 如果 input 是对象,则:
    a. 检查 input 是否有 Symbol.toPrimitive 方法。如果有,调用 input[Symbol.toPrimitive](hint),其中 hint 是根据 preferredType 确定的字符串("number", "string", 或 "default")。如果此方法返回一个原始值,则返回该值。
    b. 如果 Symbol.toPrimitive 不存在或未返回原始值:
    i. 如果 preferredType"string"hint"default"(且不是 Date 对象):

    1. 尝试调用 input.toString()。如果返回一个原始值,则返回该值。
    2. 否则,尝试调用 input.valueOf()。如果返回一个原始值,则返回该值。
      ii. 如果 preferredType"number"
    3. 尝试调用 input.valueOf()。如果返回一个原始值,则返回该值。
    4. 否则,尝试调用 input.toString()。如果返回一个原始值,则返回该值。
      c. 如果上述所有方法都未能产生原始值,则抛出 TypeError

hint 的确定:

  • 如果 preferredType"string", hint"string".
  • 如果 preferredType"number", hint"number".
  • 如果 preferredType 未指定:
    • 对于 Date 对象,hint"string".
    • 对于其他对象,hint"number".

代码示例:

// 默认 hint 为 "number"
console.log(String([]));           // ""
console.log([].valueOf());         // []
console.log(String([1]));          // "1"
console.log([1].valueOf());        // [1]

// ToPrimitive([]) (preferredType unspecified, defaults to "number")
// 1. valueOf() -> [] (非原始值)
// 2. toString() -> "" (原始值)
console.log(String([])); // ""

// ToPrimitive([5]) (preferredType unspecified, defaults to "number")
// 1. valueOf() -> [5] (非原始值)
// 2. toString() -> "5" (原始值)
console.log(String([5])); // "5"

// ToPrimitive({}) (preferredType unspecified, defaults to "number")
// 1. valueOf() -> {} (非原始值)
// 2. toString() -> "[object Object]" (原始值)
console.log(String({})); // "[object Object]"

// 对于 Date 对象,默认 hint 为 "string"
const d = new Date(0);
console.log(d.toString());      // "Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间)" (取决于时区)
console.log(d.valueOf());       // 0 (时间戳)
// ToPrimitive(new Date(0)) (preferredType unspecified, defaults to "string")
// 1. toString() -> "Thu Jan..." (原始值)
console.log(String(new Date(0))); // "Thu Jan 01 1970..."

// 如果明确指定 preferredType
// ToPrimitive(d, "number")
// 1. valueOf() -> 0 (原始值)
console.log(Number(d)); // 0

// 自定义 Symbol.toPrimitive
const objWithToPrimitive = {
    [Symbol.toPrimitive](hint) {
        if (hint === 'number') {
            return 100;
        }
        if (hint === 'string') {
            return 'hello';
        }
        return true; // default
    },
    valueOf() { return 10; },
    toString() { return 'world'; }
};

console.log(String(objWithToPrimitive)); // "hello" (hint "string")
console.log(Number(objWithToPrimitive)); // 100 (hint "number")
console.log(objWithToPrimitive + "");    // "true" (hint "default", used for `+` when one operand is object)
console.log(objWithToPrimitive == 100);  // true (ToPrimitive(obj, "number") -> 100)
console.log(objWithToPrimitive == 'hello'); // true (ToPrimitive(obj, "number") -> 100, then 100 == 'hello' -> false. Wait, this is tricky!
                                          // The `==` algorithm (step 14) calls `ToPrimitive(x)`, which defaults to "number" hint for non-Date objects.
                                          // So, `objWithToPrimitive == 'hello'` means `100 == 'hello'`, which is `100 == ToNumber('hello')` -> `100 == NaN` -> `false`.
                                          // This highlights the importance of the *default* hint for `ToPrimitive` within `==`.
                                          // Let's re-verify:
                                          // `objWithToPrimitive == 'hello'` -> `ToPrimitive(objWithToPrimitive) == 'hello'` (default hint "number")
                                          // -> `100 == 'hello'`
                                          // -> `100 == ToNumber('hello')`
                                          // -> `100 == NaN`
                                          // -> `false`
                                          // This confirms `Symbol.toPrimitive` is called with "number" hint for `==` for non-Date objects,
                                          // and then the result is used for further comparison.

4. 全量递归逻辑的典型示例

现在,让我们结合抽象相等算法和辅助操作,剖析几个经典的、具有递归性质的 == 比较。

示例 1: true == '1'

  1. true == '1'
    • Type(x)BooleanType(y)String
    • 根据算法步骤 8 (If Type(x) is Boolean), 执行 ToNumber(x) == y
    • ToNumber(true) -> 1
    • 所以变成 1 == '1'
  2. 1 == '1'
    • Type(x)NumberType(y)String
    • 根据算法步骤 4 (If Type(x) is Number and Type(y) is String), 执行 x == ToNumber(y)
    • ToNumber('1') -> 1
    • 所以变成 1 == 1
  3. 1 == 1
    • Type(x)NumberType(y)Number
    • 根据算法步骤 1 (If Type(x) is Type(y)) 中的 Number 规则,xy 值相等。
    • 返回 true

最终结果: true

示例 2: [5] == 5

  1. [5] == 5
    • Type(x)ObjectType(y)Number
    • 根据算法步骤 14 (If Type(x) is Object and Type(y) is String or Number or Symbol or BigInt), 执行 ToPrimitive(x) == y
    • ToPrimitive([5]) (默认 hint: "number"):
      • [5].valueOf() -> [5] (非原始值)
      • [5].toString() -> '5' (原始值)
    • 所以 ToPrimitive([5]) 返回 '5'
    • 比较变成 '5' == 5
  2. '5' == 5
    • Type(x)StringType(y)Number
    • 根据算法步骤 5 (If Type(x) is String and Type(y) is Number), 执行 ToNumber(x) == y
    • ToNumber('5') -> 5
    • 所以比较变成 5 == 5
  3. 5 == 5
    • Type(x)NumberType(y)Number
    • 根据算法步骤 1 (If Type(x) is Type(y)) 中的 Number 规则,xy 值相等。
    • 返回 true

最终结果: true

示例 3: new Date(0) == 0

  1. new Date(0) == 0
    • Type(x)Object (Date object),Type(y)Number
    • 根据算法步骤 14 (If Type(x) is Object and Type(y) is String or Number or Symbol or BigInt), 执行 ToPrimitive(x) == y
    • ToPrimitive(new Date(0)) (对于 Date 对象,默认 hint: "string"):
      • Date(0).toString() -> "Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间)" (原始值)
    • 注意! 这里的 ToPrimitive 默认 hint"string"。如果 new Date(0)toString() 方法返回了原始值,那么它就会被用作转换结果。
    • 所以,比较变成 "Thu Jan 01 1970..." == 0
    • 等等,这里有一个陷阱!ToPrimitivehint 未指定时,对于 Date 对象,它的 hint 确实是 "string"。但是,当 == 算法在步骤 14 中调用 ToPrimitive(x) 时,它 不会 传入 preferredType。这意味着 ToPrimitive 内部会根据 xDate 对象来决定其 hint"string",否则为 "number"
    • 然而,ECMAScript 规范在 ToPrimitive 的调用中,当 ObjectNumber 比较时,是带有 "number" 提示的。
    • 我们来查阅ECMA-262 (12.10.1 Abstract Equality Comparison) 步骤 14:
      Return the result of the comparison ToPrimitive(x, Number) == y.
      这明确指出 ToPrimitive 被调用时带有 Number 提示。
    • 纠正:
      ToPrimitive(new Date(0), hint: "number")

      • Date(0).valueOf() -> 0 (原始值)
      • 所以 ToPrimitive(new Date(0), "number") 返回 0
    • 比较变成 0 == 0
  2. 0 == 0
    • Type(x)NumberType(y)Number
    • 根据算法步骤 1 (If Type(x) is Type(y)) 中的 Number 规则,xy 值相等。
    • 返回 true

最终结果: true

这个例子很好地说明了理解规范细节的重要性,尤其是 ToPrimitivehint 机制在不同上下文中的行为。

示例 4: '' == false

  1. '' == false
    • Type(x)StringType(y)Boolean
    • 根据算法步骤 9 (If Type(y) is Boolean), 执行 x == ToNumber(y)
    • ToNumber(false) -> 0
    • 所以比较变成 '' == 0
  2. '' == 0
    • Type(x)StringType(y)Number
    • 根据算法步骤 5 (If Type(x) is String and Type(y) is Number), 执行 ToNumber(x) == y
    • ToNumber('') -> 0
    • 所以比较变成 0 == 0
  3. 0 == 0
    • Type(x)NumberType(y)Number
    • 根据算法步骤 1 (If Type(x) is Type(y)) 中的 Number 规则,xy 值相等。
    • 返回 true

最终结果: true

示例 5: [] == ![]

这是一个经典的面试题,涉及运算符优先级和抽象相等。

  1. [] == ![]
    • 首先计算 ![]
    • ! 操作符会首先将操作数转换为布尔值 (ToBoolean([]) -> true),然后取反。
    • 所以 ![] -> !true -> false
    • 比较变成 [] == false
  2. [] == false
    • Type(x)ObjectType(y)Boolean
    • 根据算法步骤 14 (If Type(x) is Object and Type(y) is String or Number or Symbol or BigInt), y 不是 String, Number, Symbol, BigInt。所以这条规则不适用。
    • 根据算法步骤 9 (If Type(y) is Boolean), 执行 x == ToNumber(y)
    • ToNumber(false) -> 0
    • 所以比较变成 [] == 0
  3. [] == 0
    • Type(x)ObjectType(y)Number
    • 根据算法步骤 14 (If Type(x) is Object and Type(y) is String or Number or Symbol or BigInt), 执行 ToPrimitive(x) == y
    • ToPrimitive([]) (默认 hint: "number"):
      • [].valueOf() -> [] (非原始值)
      • [].toString() -> '' (原始值)
    • 所以 ToPrimitive([]) 返回 ''
    • 比较变成 '' == 0
  4. '' == 0
    • Type(x)StringType(y)Number
    • 根据算法步骤 5 (If Type(x) is String and Type(y) is Number), 执行 ToNumber(x) == y
    • ToNumber('') -> 0
    • 所以比较变成 0 == 0
  5. 0 == 0
    • Type(x)NumberType(y)Number
    • 根据算法步骤 1 (If Type(x) is Type(y)) 中的 Number 规则,xy 值相等。
    • 返回 true

最终结果: true


5. 抽象相等中的特殊情况和常见误解

  • nullundefined 的独特性:它们只在彼此之间相等,不与任何其他值(包括 0, false, '')相等。

    console.log(null == undefined); // true
    console.log(null == 0);         // false
    console.log(null == '');        // false
    console.log(null == false);     // false
    console.log(undefined == 0);    // false

    这在算法中由步骤 23 明确规定,并且在后续的 ToNumber 转换中,null 会转换为 0undefined 会转换为 NaN,但这些转换发生在 null/undefined 之间的特殊规则之后。

  • NaN 的行为NaN 是唯一一个不等于自身的值,即使是 == 也如此。

    console.log(NaN == NaN); // false
    console.log(NaN == 0);   // false

    这在算法中由步骤 1Number 规则明确规定。

  • 对象与对象比较:当两个操作数都是对象且类型相同时,== 的行为与 === 相同,即比较引用。只有当它们指向同一个内存地址时才相等。

    console.log({} == {});         // false
    let obj = {};
    console.log(obj == obj);       // true

    这在算法中由步骤 1Object 规则处理。但如果一个对象与一个原始类型比较,就会触发 ToPrimitive 转换。

  • Symbol 类型的限制Symbol 值在 == 比较中非常严格。它只与自身严格相等。与其他任何类型(包括字符串、数字或对象)进行 == 比较时,通常会返回 false,因为没有定义有意义的类型转换。

    console.log(Symbol('a') == Symbol('a')); // false (不同引用)
    let s = Symbol('a');
    console.log(s == s);                     // true
    console.log(Symbol('a') == 'a');         // false (步骤10/11)
    console.log(Symbol('a') == 0);           // false (步骤10/11)
    console.log(Symbol('a') == {});          // false (步骤10/11)
  • BigIntNumber 之间的转换BigIntNumber 之间不能直接通过 ToNumberToBigInt 进行转换(它们会抛出 TypeError)。== 算法为它们定义了特殊的比较规则(步骤 1213),即在数值上相等则返回 true,但有 NaNInfinity 的例外。

    console.log(10n == 10);     // true
    console.log(10n == 10.0);   // true
    console.log(10n == 10.1);   // false
    console.log(10n == NaN);    // false
    console.log(10n == Infinity); // false

6. 何时以及为何倾向于使用 ===

通过对抽象相等转义算法的深入剖析,我们不难发现 == 的行为是多么复杂和微妙。这种复杂性在许多情况下会导致意想不到的结果,增加代码的理解难度和潜在的bug。

因此,JavaScript社区普遍推荐在大多数情况下使用 ===(严格相等)操作符。原因如下:

  • 可预测性=== 不执行任何类型转换。它只在操作数的类型和值都严格相同时才返回 true。这使得其行为非常容易预测和理解。
  • 减少错误:避免了隐式类型转换带来的陷阱,例如 '' == false0 == false 这样的结果可能会让初学者感到困惑。
  • 提高可读性:代码意图更清晰。当看到 === 时,你知道比较的是值和类型;当看到 == 时,则需要在大脑中执行一遍复杂的抽象相等算法。

尽管如此,理解 == 的工作原理仍然至关重要。它不仅能帮助你阅读和调试遗留代码,还能让你在某些特定场景下(例如,当你知道并确实需要利用类型转换时)做出更明智的选择。例如,在处理表单输入时,用户输入的字符串数字与实际的数字进行比较,== 可能会显得方便,但通常更推荐显式地进行类型转换(如 Number(input) === actualValue)。


7. 深入理解是掌握复杂性的关键

抽象相等比较算法是JavaScript语言设计中一个充满历史沉淀和工程权衡的特性。它并非简单地将所有值都转换为数字进行比较,而是一套精心设计的、多阶段、递归的规则体系。从 null/undefined 的特殊处理,到原始类型之间的转换偏好,再到对象转换为原始值的复杂过程,每一步都体现了语言在灵活性和一致性之间的平衡。深入理解这些机制,不仅能帮助我们写出更健壮、更可预测的代码,更能提升我们对JavaScript这门语言的认知深度,从而更好地驾驭其独特的魅力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注