使用 JavaScript 实现抽象相等比较(==)
抽象相等比较(Abstract Equality Comparison
)即 ==
操作符,又被称作宽松相等、非严格相等。
而在抽象相等比较的过程中,为了使 ==
两侧的数据可以进行比较,会尽可能将它们转换成相同类型,这就是的隐式类型转换。
问题
先来看看 ==
那些令人困惑的例子:
1 | console.log([] == ![]) // true |
让我们根据 ECMA-262 Abstract Equality Comparison 实现一个抽象相等比较吧,以此来了解比较的过程中发生了什么。
实现
AbstractEqualityComparison( x, y )
抽象相等比较的主体。
规范
原文
If Type(x) is the same as Type(y), then
a. Return the result of performing Strict Equality Comparison x === y.
If x is null and y is undefined, return true.
If x is undefined and y is null, return true.
If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
If Type(x) is BigInt and Type(y) is String, then
a. Let n be ! StringToBigInt(y).
b. If n is NaN, return false.
c. Return the result of the comparison x == n.
If Type(x) is String and Type(y) is BigInt, return the result of the comparison y == x.
If Type(x) is Boolean, return the result of the comparison ! ToNumber(x) == y.
If Type(y) is Boolean, return the result of the comparison x == ! ToNumber(y).
If Type(x) is either String, Number, BigInt, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
If Type(x) is Object and Type(y) is either String, Number, BigInt, or Symbol, return the result of the comparison ToPrimitive(x) == y.
If Type(x) is BigInt and Type(y) is Number, or if Type(x) is Number and Type(y) is BigInt, then
a. If x or y are any of NaN, +∞, or -∞, return false.
b. If the mathematical value of x is equal to the mathematical value of y, return true; otherwise return false.
Return false.
注:
x ==! ToNumber(x)
中的!
的意义我不清楚,但早期的规范里是没有!
的,且忽略后逻辑是通顺的,后续都将按忽略处理。
译文
当 Type(x) 和 Type(y) 相同时,则
a. 返回 x === y。
当 x 是 null 并且 y 是 undefined 时 true 。
当 x 是 undefined 并且 y 是 null 时 true 。
当 Type(x) 是 Number 并且 Type(y) 是 String 时,返回 x == ToNumber(y) 。
当 Type(x) 是 String 并且 Type(y) 是 Number 时,返回 ToNumber(x) == y 。
当 Type(x) 是 BigInt 并且 Type(y) 是 String 时,则
a. 令 n = StringToBigInt(y) 。
b. 当 n 是 NaN 时,返回 false 。
c. 返回 x == n 。
当 Type(x) 是 String 并且 Type(y) 是 BigInt 时,返回 y == x 。
当 Type(x) 是 Boolean 时,返回 ToNumber(x) == y 。
当 Type(y) 是 Boolean 时,返回 x == ToNumbery) 。
当 Type(x) 是 String, Number, BigInt 或 Symbol 并且 Type(y) 是 Object 时,返回 x == ToPrimitive(y) 。
当 Type(x) 是 Object 并且 Type(y) 是 String, Number, BigInt 或 Symbol 时,返回 ToPrimitive(x) == y 。
当 Type(x) 是 BigInt 并且 Type(y) 是 Number 时,或者 当 Type(y) 是 Number 并且 Type(y) 是 BigInt 时,则
a. 当 x 或 y 是 NaN, +∞, or -∞ 时,返回 false 。
b. 当 x 的数值等于 y 的数值时,返回 true ,反之 false。
返回 false 。
代码
最费解的想必就是 ToPrimitive
了,我们可以先不管它,先完成主体。
Type
, ToNumber
, ToPrimitive
等将会在后续再进行说明。
Number
和 BigInt
的数值比较,可以通过 toString(2)
将它们转成二进制字符串,再进行比较。
1 | function AbstractEqualityComparison(x, y) { |
注:在 12.a
中的 includes
不可使用 indexOf
代替,[NaN].indexOf(NaN)
始终返回 -1
。
Type( argument )
Type
的作用是获取数据的类型。
规范
原文
An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Symbol, Number, BigInt, and Object. An ECMAScript language value is a value that is characterized by an ECMAScript language type.
代码
typeof
大致和 Type
一样,
但 typeof
会将 null
认为是 object
,把函数认为 function
,
单独对这两种类型做调整即可。
1 | function Type(argument) { |
ToNumber( argument )
ToNumber
的作用是将其他类型转换成 Number
类型。
规范
原文
The abstract operation ToNumber converts argument to a value of type Number according to Table 11:
Table 11: ToNumber Conversions
Argument Type Result Undefined Return NaN. Null Return +0. Boolean If argument is true, return 1. If argument is false, return +0. Number Return argument (no conversion). String See grammar and conversion algorithm below. Symbol Throw a TypeError exception. BigInt Throw a TypeError exception. Object Apply the following steps:Let primValue be ? ToPrimitive(argument, hint Number).Return ? ToNumber(primValue).
译文
ToNumber 根据表 11将 argument 转换成 Number 类型的值:
表 11: ToNumber 转换
Argument 类型 结果 Undefined 返回 NaN. Null 返回 +0。 Boolean 当 argument 是 true 时,返回 1 。当 argument 是 false 时,返回 +0 。 Number 返回 argument (不进行转换)。 String 见下方语法和转换算法。 Symbol 抛出 TypeError 。 BigInt 抛出 TypeError 。 Object 应用以下步骤:
1. 令 primValue = ToPrimitive(argument, hint Number)。
2. 返回 ToNumber(primValue) 。
代码
ToNumber
与Number
几乎一致,只不过对于BigInt
,ToNumber
会直接抛出类型错误。
1 | function ToNumber(argument) { |
StringToBigInt( argument )
StringToBigInt
的作用是将 String
转成 BigInt
。
代码
BigInt
中 String
的解析器想必使用的就是 StringToBigInt
。
在 BigInt
中当 StringToBigInt
返回值如果是 NaN
则会直接抛出错误。
借用 BigInt
实现 StringToBigInt
则就是捕获错误,返回 NaN
。
1 | function StringToBigInt(argument) { |
ToPrimitive( input [, PreferredType ] )
ToPrimitive
的作用是将输入的值转成值类型(除了 Object
以外的基础类型)。
规范
原文
Assert: input is an ECMAScript language value.
If Type(input) is Object, then
a. If PreferredType is not present, let hint be “default”.
b. Else if PreferredType is hint String, let hint be “string”.
c. Else,
i. Assert: PreferredType is hint Number.
ii. Let hint be “number”.
d. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
e. If exoticToPrim is not undefined, then
i. Let result be ? Call(exoticToPrim, input, « hint »).
ii. If Type(result) is not Object, return result.
iii. Throw a TypeError exception.
f. If hint is “default”, set hint to “number”.
g. Return ? OrdinaryToPrimitive(input, hint).
Return input.
译文
断言: input 是一个 ECMAScript 语言的值.
当 Type(input) 是 Object 时,则
a. 当 PreferredType 不存在时,令 hint = “default” 。
b. 否则当 PreferredType 是 String 时,令 hint = “string” 。
c. 否则,
i. 断言: PreferredType 是 Number 。
ii. 令 hint = “number” 。
d. 令 exoticToPrim = GetMethod(input, @@toPrimitive) 。
e. 当 exoticToPrim 不是 undefined 时,则
i. 令 result = Call(exoticToPrim, input, « hint ») 。
ii. 当 Type(result) 不是 Object 时,返回 result 。
iii. 抛出 TypeError 。
f. 当 hint 是 “default” 时,令 hint = “number” 。
g. 返回 OrdinaryToPrimitive(input, hint) 。
返回 input 。
代码
1 | function ToPrimitive(input, PreferredType) { |
OrdinaryToPrimitive( o, hint )
OrdinaryToPrimitive
是将 Object
转成值类型。
参数 hint
控制优先使用 toString
还是 valueOf
。
规范
原文
Assert: Type(hint) is String and its value is either “string” or “number”.
If hint is “string”, then
a. Let methodNames be « “toString”, “valueOf” ».
Else,
a. Let methodNames be « “valueOf”, “toString” ».
For each name in methodNames in List order, do
a. Let method be ? Get(O, name).
b. If IsCallable(method) is true, then
i. Let result be ? Call(method, O).
ii. If Type(result) is not Object, return result.
Throw a TypeError exception.
译文
断言: Type(O) 是 Object 。
断言: Type(hint) 是 String 并且 hint 的值 是 “string” 或 “number” 。
当 hint 是 “string” 时,则
a. 令 methodNames = « “toString”, “valueOf” »。
否则,
a. 令 methodNames = « “valueOf”, “toString” »。
遍历 methodNames , 执行
a. 令 method = Get(O, name) 。
b. 当 IsCallable(method) 是 true 时,则
i. 令 result = Call(method, O) 。
ii. 当 Type(result) 不是 Object 时,返回 result 。
抛出 TypeError 。
代码
1 | function OrdinaryToPrimitive(o, hint) { |
其他
1 | function GetMethod(v, p) { |
测试
1 | function test(x, y) { |
1 | const testData1 = [ |
结语
在实际开发中,是要尽可能避免使用 ==
操作符,有人可能觉得这篇文章没有意义。其实我想传达的是一种学习方法,可以通过相同的方式学习 JavaScript
的其他内容。
最后,上面实现的 AbstractEqualityComparison
还是有用处的,我在上面代码的基础上写了个 抽象相等比较过程展示 ,能更清楚地看到比较的过程。不妨输入文章开头的问题,看看比较的过程吧。