JavaScript开发人员Douglas Crockford将JavaScript的==
和!=
运算符称为邪恶孪生子,应避免使用 。 但是,一旦您了解了它们,这些运算符就不会那么糟,并且实际上可能会有用。 本文研究==
和!=
,解释它们如何工作,并帮助您更好地了解它们。
有问题的==
和!=
运算符
JavaScript语言包含两组相等运算符: ===
和!==
,以及
==
和!=
。 了解为什么有两套相等运算符,并弄清楚在哪种情况下要使用哪些运算符引起了很多混乱。
===
和!==
运算符并不难理解。 当两个操作数具有相同的类型并具有相同的值时, ===
返回true
,而!==
返回false
。 但是,当值或类型不同时, ===
返回false
, !==
返回true
。
当两个操作数具有相同的类型时, ==
和!=
运算符的行为方式相同。 但是,当类型不同时,JavaScript 会将操作数强制转换为另一种类型,以使操作数在比较之前兼容。 结果通常令人困惑,如下所示:
"this_is_true" == false // false
"this_is_true" == true // false
因为只有两个可能的布尔值,所以您可能会认为其中一个表达式的值应为true
。 但是,它们都评估为false
。 当您假设传递关系时(如果a等于b且b等于c则a等于c),还会发生其他混乱:
'' == 0 // true
0 == '0' // true
'' == '0' // false
此示例表明==
缺少传递性。 如果空字符串等于数字0,并且数字0等于由字符0组成的字符串,则空字符串应等于由0组成的字符串。但事实并非如此。
当通过==
或!=
比较操作数时遇到不兼容的类型时,JavaScript会将一种类型强制转换为另一种类型以使其具有可比性。 相反,在使用===
和!==
时,它从不执行类型强制(这会导致更好的性能)。 由于类型不同,在第二个示例中===
总是返回false
。
了解控制JavaScript如何将操作数强制转换为其他类型的规则,以便在应用==
和!=
之前这两个操作数都是类型兼容的,这可以帮助您确定何时更适合使用==
和!=
,并充满信心使用这些运算符。 在下一节中,我们将探讨与==
和!=
运算符一起使用的强制规则。
==
和!=
如何工作?
学习==
和!=
的最佳方法是研究ECMAScript语言规范。 本节重点介绍ECMAScript 262 。 规范的第11.9节介绍了相等运算符。
==
和!=
运算符出现在语法产生式EqualityExpression
和EqualityExpressionNoIn
。 (与第一个生产不同,第二个生产避免了in
运算符。)让我们检查一下EqualityExpression
生产,如下所示。
EqualityExpression :
RelationalExpression
EqualityExpression == RelationalExpression
EqualityExpression != RelationalExpression
EqualityExpression === RelationalExpression
EqualityExpression !== RelationalExpression
根据该结果,等式表达式可以是一个关系表达式,一个等式表达式通过==
等于一个关系表达式,一个等式表达式通过!=
等于一个关系表达式,等等。 (我忽略了===
和!==
,它们与本文无关。)
第11.9.1节介绍了有关==
工作方式的以下信息:
生产EqualityExpression: EqualityExpression == RelationalExpression的评估如下:
- 令lref为评估EqualityExpression的结果。
- 令lval为GetValue( lref )。
- 令rref为评估RelationalExpression的结果。
- 令rval为GetValue( rref )。
- 返回执行抽象相等比较的结果rval == lval 。 (请参阅11.9.3。)
第11.9.2节提供了有关!=
类似信息:
生产EqualityExpression: EqualityExpression != RelationalExpression的评估如下:
- 令lref为评估EqualityExpression的结果。
- 令lval为GetValue( lref )。
- 令rref为评估RelationalExpression的结果。
- 令rval为GetValue( rref )。
- 令r为执行抽象相等比较rval != lval的结果 。 (请参阅11.9.3。)
- 如果r为true ,则返回false 。 否则,返回true 。
lref
和rref
是==
和!=
运算符左右两侧的引用。 每个引用都传递给GetValue()
内部函数以返回相应的值。
==
和!=
工作方式由11.9.3节中介绍的抽象相等比较算法指定:
比较
x == y
,其中x
和y
是值,产生
是非 题 。 这样的比较执行如下:
- 如果Type(
x
)与Type(y
)相同,则
- 如果Type(
x
)是Undefined,则返回true 。- 如果Type(
x
)为Null,则返回true 。- 如果Type(
x
)是Number,则
- 如果
x
为NaN ,则返回false 。- 如果
y
为NaN ,则返回false 。- 如果x与y相同,则返回true 。
- 如果x为+0且y为-0 ,则返回true 。
- 如果x为-0且y为+0 ,则返回true 。
- 返回false 。
- 如果Type(
x
)是String,则如果x
和y
是完全相同的字符序列(相同的长度和相同位置的相同字符),则返回true 。 否则,返回false 。- 如果Type(
x
)为布尔值,则如果x
和y
均为true或false则返回true 。 否则,返回false 。- 如果
x
和y
指向同一对象,则返回true 。 否则,返回false 。- 如果
x
为null且y
未定义 ,则返回true 。- 如果
x
未定义且y
为null ,则返回true。- 如果Type(
x
)是Number并且Type(y
)是String,则返回比较结果x
== ToNumber(y
)。- 如果Type(
x
)是String且Type(y
)是Number,则返回比较结果ToNumber(x
)==y
。- 如果Type(
x
)为布尔值,则返回比较结果ToNumber(x
)==y
。- 如果Type(
y
)为布尔型,则返回比较结果x
== ToNumber(y
)。- 如果Type(
x
)是String或Number且Type(y
)是Object,则返回比较结果x
== ToPrimitive(y
)。- 如果Type(
x
)是Object并且Type(y
)是String或Number,则返回比较结果ToPrimitive(x
)==y
。- 返回false 。
当操作数类型相同时,执行此算法的步骤1。 它显示undefined
等于undefined
, null
等于null
。 它还显示了什么都不等于NaN
(非数字),两个相同的数值相等,+ 0等于-0,两个长度相同且字符序列相同的字符串, true
等于true
和false
等于false
以及对两个的引用同一对象相等。
步骤2和3显示了为什么null != undefined
返回false
。 JavaScript认为这些值是相同的。
从步骤4开始,该算法变得很有趣。 此步骤集中于Number和String值之间的相等性。 当第一个操作数是数字而第二个操作数是字符串时,第二个操作数通过ToNumber()
内部函数转换为数字。 表达式x
== ToNumber( y
)表示递归; 重新应用从11.9.1节开始的算法。
步骤5等效于步骤4,但第一个操作数的类型为String,必须将其转换为Number类型。
步骤6和7将布尔操作数转换为Number类型并递归。 如果另一个操作数是布尔值,则在下次执行此算法时将其转换为数字,这将再次递归一次。 从性能的角度来看,您可能要确保两个操作数均为布尔类型,以避免两个递归步骤。
步骤9显示,如果其中一个操作数是对象类型,则该操作数将通过以下方式转换为原始值:
ToPrimitive()
内部函数和算法递归。
最后,该算法认为两个操作数都不相等,并在步骤10中返回false
。
尽管很详细,但是抽象相等比较算法相当容易遵循。 但是,它指的是一对内部函数ToNumber()
和ToPrimitive()
,需要公开其内部工作原理以全面了解算法。
ToNumber()
函数将其参数转换为Number,并在9.3节中进行介绍。 以下列表总结了可能的非数字参数和等效的返回值:
- 如果参数为Undefined,则返回NaN 。
- 如果参数为Null,则返回+0 。
- 如果参数为Boolean true,则返回1 。 如果参数为布尔型false,则返回+0 。
- 如果参数为Number类型,则返回输入参数-没有转换。
- 如果参数具有字符串类型,则适用第9.3.1节“应用于字符串类型的ToNumber”。 返回与语法指示的字符串参数相对应的数值。 如果参数不符合指示的语法,则返回NaN。 例如,参数
"xyz"
导致返回NaN。 同样,参数"29"
导致返回29。 - 如果参数具有对象类型,则执行以下步骤:
- 设primValue为ToPrimitive( 输入参数 ,提示编号)。
- 返回ToNumber( primValue )。
ToPrimitive()
函数接受一个输入参数和一个可选的PreferredType参数。 输入参数将转换为非对象类型。 如果一个对象能够转换为多个原始类型,则ToPrimitive()
使用可选的PreferredType提示来偏爱该首选类型。 转换发生如下:
- 如果输入参数为未定义,则返回输入参数(未定义)-不进行转换。
- 如果输入参数为Null,则返回输入参数(Null)—不进行转换。
- 如果输入参数为布尔类型,则返回输入参数-不进行转换。
- 如果输入参数为Number类型,则返回输入参数-不进行转换。
- 如果输入参数为String类型,则返回输入参数-不进行转换。
- 如果输入参数具有对象类型,则返回与输入参数对应的默认值。 通过调用对象的
[[DefaultValue]]
内部方法并传递可选的PreferredType提示来检索对象的[[DefaultValue]]
。 在第8.12.8节中为所有本机ECMAScript对象定义了[[DefaultValue]]
的行为。
本节介绍了大量理论。 在下一部分中,我们将通过介绍涉及==
和!=
各种表达式,并逐步执行算法步骤来对其进行评估,来着手实践。
认识邪恶的双胞胎
既然我们知道==
和!=
如何根据ECMAScript规范工作,那么让我们通过探索涉及这些运算符的各种表达式来充分利用此知识。 我们将逐步介绍如何评估这些表达式,并找出它们为什么是true
或false
。
对于我的第一个示例,请考虑在本文开头附近出现的以下对或表达式:
"this_is_true" == false // false
"this_is_true" == true // false
请按照以下步骤,根据抽象相等比较算法对这些表达式求值:
- 跳过步骤1,因为类型不同:
typeof "this_is_true"
返回"string"
,typeof false
或typeof true
返回"boolean"
。 - 跳过第2步到第6步,该步骤不适用,因为它们与操作数类型不匹配。 但是,因为正确的参数是布尔类型,所以适用步骤7。 表达式将转换为
"this_is_true" == ToNumber(false)
和"this_is_true" == ToNumber(true)
。 -
ToNumber(false)
返回+0,而ToNumber(true)
返回1,这将表达式分别减小为"this_is_true" == +0
和"this_is_true" == 1
。 在这一点上,算法递归。 - 跳过第1步至第4步,该步骤不适用。 但是,由于左侧操作数的类型为String,而右侧操作数的类型为Number,因此应用步骤5。 表达式将转换为
ToNumber("this_is_true") == +0
和ToNumber("this_is_true") == 1
。 -
ToNumber("this_is_true")
返回NaN,这会将表达式分别ToNumber("this_is_true")
为NaN == +0
和NaN == 1
。 在这一点上,算法递归。 - 输入第1步是因为NaN,+ 0和1均为数字类型。 步骤1.a和1.b被跳过,因为它们不适用。 但是,步骤1.ci适用,因为左操作数为NaN。 该算法现在返回false(NaN不等于包括自身在内的任何值)作为每个原始表达式的值,并倒回堆栈以完全退出递归。
我的第二个示例(根据《银河漫游指南》基于生命的意义)通过==
将对象与数字进行比较,返回值为true
:
var lifeAnswer = {
toString: function() {
return "42";
}
};
alert(lifeAnswer == 42);
以下步骤显示了JavaScript如何使用抽象相等比较算法来达到表达式的真实值:
- 跳过第1步至第8步,该步骤不适用,因为它们与操作数类型不匹配。 但是,步骤9适用,因为左操作数的类型为Object,而右操作数的类型为Number。 该表达式将转换为
ToPrimitive(lifeAnswer) == 42
。 -
ToPrimitive()
调用lifeAnswer
的[[DefaultValue]]
内部方法而没有任何提示。 根据ECMAScript 262规范的8.12.8节,[[DefaultValue]]
调用toString()
方法,该方法返回"42"
。 将该表达式转换为"42" == 42
,然后递归算法。 - 跳过第1步到第4步,该步骤不适用,因为它们与操作数类型不匹配。 但是,由于左侧操作数的类型为String,而右侧操作数的类型为Number,因此应用步骤5。 该表达式将转换为
ToNumber("42") == 42
。 -
ToNumber("42")
返回42,并将表达式转换为42 ==42。算法递归并执行步骤1.c.iii。 因为数字相同,所以返回true
,并且递归展开。
对于我的最后一个示例,让我们弄清楚为什么以下序列没有展示可传递性,在该传递中,第三次比较将返回true
而不是false
:
'' == 0 // true
0 == '0' // true
'' == '0' // false
下列步骤显示的JavaScript如何使用抽象平等比较算法在到达true
的价值'' == 0
。
- 执行第5步,得出
ToNumber('') == 0
,将其转换为0 == 0
,然后递归算法。 (规范中的9.3.1节指出, StringNumericLiteral ::: [空]的MV [数学值]为0。换句话说,空字符串的数值为0。) - 步骤1.c.iii执行,将0与0进行比较并返回
true
(并展开递归)。
以下步骤显示JavaScript如何使用Abstract Equality比较算法来实现值为0 == '0'
true
:
- 步骤4的执行结果为
0 == ToNumber('0')
,将其转换为0 == 0
,然后递归算法。 - 步骤1.c.iii执行,将0与0进行比较并返回
true
(并展开递归)。
最后,JavaScript执行Abstract Equality比较算法中的步骤1.d,得出true
作为'' == '0'
。 因为两个字符串的长度(0和1)不同,所以返回false
。
结论
也许您想知道为什么要麻烦==
和!=
。 毕竟,先前的示例表明,由于类型强制和递归,这些运算符的速度可能比其===
和!==
慢。 您可能要使用==
和!=
因为在某些情况下===
和!==
没有优势。 考虑以下示例:
typeof lifeAnswer === "object"
typeof lifeAnswer == "object"
typeof
运算符返回一个String值。 因为将一个String值与另一个String值( "object"
)进行比较,所以不会发生类型强制,并且==
与===
一样有效。 也许从未接触过===
JavaScript新手会发现这种代码更清晰。 类似地,以下代码片段不需要类型强制(两个操作数都具有Number类型),因此!=
效率不低于!==
:
array.length !== 3
array.length != 3
这些示例表明==
和!=
适用于不需要强制的比较。 当操作数类型不同时, ===
和!==
是===
的方法,因为它们返回false
而不是意外值(例如false == ""
返回true
)。 如果操作数类型相同,则没有理由不使用==
和!=
。 也许是时候停止惧怕邪恶的双胞胎了,在您了解它们之后,它们并没有那么邪恶。