JavaScript开发者Douglas Crockford曾将JavaScript的==
和!=
运算符称为应该避免的“邪恶双胞胎”。然而,一旦你理解了它们,这些运算符并没有那么糟糕,实际上可能很有用。本文将探讨==
和!=
,解释它们的工作原理,并帮助你更好地了解它们。
关键要点
==
和!=
运算符并非天生邪恶;它们在比较不同类型的值时执行类型强制转换,这既有用又棘手。===
和!==
进行直接的类型和值比较,无需强制转换,这更清晰,通常建议避免意外结果。当需要类型强制转换或比较类型可能动态变化的值时,使用==
和!=
。==
和!=
比较期间如何强制转换类型,以便更准确地预测结果并避免常见陷阱。==
和!=
如何在各种场景中运行,例如将字符串与数字或对象与原始值进行比较,以巩固理解。==
和!=
并非可怕,但它们需要对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)应该适用时,会出现额外的混淆:
"this_is_true" == false // false "this_is_true" == true // false
此示例表明==
缺乏传递性。如果空字符串等于数字0,并且如果数字0等于由字符0组成的字符串,则空字符串应该等于由0组成的字符串。但事实并非如此。当通过==
或!=
比较操作数时遇到不兼容的类型时,JavaScript会将一种类型强制转换为另一种类型以使其可比较。相反,在使用===
和!==
时,它永远不会执行类型强制转换(这会导致性能略微提高)。由于类型不同,===
在第二个示例中总是返回false
。理解控制JavaScript如何将操作数强制转换为不同类型以便在应用==
和!=
之前两个操作数类型兼容的规则,可以帮助你确定何时更适合使用==
和!=
,并对使用这些运算符充满信心。在下一节中,我们将探讨与==
和!=
运算符一起使用的强制转换规则。
==
和!=
如何工作?
学习==
和!=
如何工作的最佳方法是研究ECMAScript语言规范。本节重点介绍ECMAScript 262。规范的第11.9节介绍了相等运算符。==
和!=
运算符出现在语法产生式EqualityExpression
和EqualityExpressionNoIn
中。(与第一个产生式不同,第二个产生式避免了in
运算符。)让我们检查一下下面显示的EqualityExpression
产生式。
'' == 0 // true 0 == '0' // true '' == '0' // false
根据此产生式,相等表达式要么是关系表达式,要么是通过==
等于关系表达式的相等表达式,要么是通过!=
不等于关系表达式的相等表达式,等等。(我忽略了===
和!==
,它们与本文无关。)第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
是值,产生true
或false
。此类比较如下进行:
- 如果
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)
是Boolean
,则如果x
和y
都是true
或都是false
,则返回true
。否则,返回false
。- 如果
x
和y
引用同一个对象,则返回true
。否则,返回false
。- 如果
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)
是Boolean
,则返回比较ToNumber(x) == y
的结果。- 如果
Type(y)
是Boolean
,则返回比较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
值之间的相等性。当第一个操作数是Number
而第二个操作数是String
时,第二个操作数通过ToNumber()
内部函数转换为Number
。表达式x == ToNumber(y)
表示递归;从第11.9.1节开始的算法被重新应用。步骤5等效于步骤4,但第一个操作数的类型为String
,必须转换为Number
类型。步骤6和7将布尔操作数转换为Number
类型并递归。如果另一个操作数是布尔值,它将在下次执行此算法时转换为Number
,这将再次递归。从性能的角度来看,你可能希望确保两个操作数都是布尔类型以避免两个递归步骤。步骤9显示,如果任一操作数的类型为Object
,则此操作数通过ToPrimitive()
内部函数转换为原始值,并且算法递归。最后,算法认为两个操作数不相等并在步骤10中返回false
。虽然详细,但抽象相等比较算法相当容易理解。但是,它引用了一对内部函数ToNumber()
和ToPrimitive()
,其内部工作需要公开才能完全理解该算法。ToNumber()
函数将其参数转换为Number
,并在第9.3节中进行了描述。以下列表总结了可能的非数字参数和等效返回值:
Undefined
,则返回NaN
。Null
,则返回 0。true
,则返回1。如果参数是布尔值false
,则返回 0。Number
,则返回输入参数——没有转换。String
,则应用第9.3.1节“应用于字符串类型的ToNumber”。返回根据语法指示的与字符串参数相对应的数值。如果参数不符合指示的语法,则返回NaN
。例如,参数“xyz”导致返回NaN
。此外,参数“29”导致返回29。Object
,则应用以下步骤:primValue
为ToPrimitive(输入参数, 提示Number)
。ToNumber(primValue)
。ToPrimitive()
函数接受一个输入参数和一个可选的PreferredType
参数。输入参数转换为非对象类型。如果对象能够转换为多个原始类型,则ToPrimitive()
使用可选的PreferredType
提示来偏向首选类型。转换如下进行:
Undefined
,则返回输入参数(Undefined
)——没有转换。Null
,则返回输入参数(Null
)——没有转换。Boolean
,则返回输入参数——没有转换。Number
,则返回输入参数——没有转换。String
,则返回输入参数——没有转换。Object
,则返回与输入参数相对应的默认值。通过调用对象的[[DefaultValue]]
内部方法并传递可选的PreferredType
提示来检索对象的默认值。[[DefaultValue]]
的行为在第8.12.8节中为所有原生ECMAScript对象定义。本节介绍了相当多的理论。在下一节中,我们将通过提供涉及==
和!=
的各种表达式并逐步完成算法步骤来评估它们,从而转向实践。
了解邪恶双胞胎
现在我们已经根据ECMAScript规范了解了==
和!=
的工作原理,让我们通过探索涉及这些运算符的各种表达式来充分利用这些知识。我们将逐步介绍如何评估这些表达式,并找出它们为什么是true
或false
。对于我的第一个示例,请考虑文章开头附近介绍的以下表达式对:
"this_is_true" == false // false "this_is_true" == true // false
按照抽象相等比较算法,按照以下步骤评估这些表达式:
typeof "this_is_true"
返回“string”,而typeof false
或typeof true
返回“boolean”。Boolean
。表达式转换为"this_is_true" == ToNumber(false)
和"this_is_true" == ToNumber(true)
。ToNumber(false)
返回 0,ToNumber(true)
返回1,这将表达式分别简化为"this_is_true" == 0
和"this_is_true" == 1
。此时算法递归。String
,右操作数的类型为Number
。表达式转换为ToNumber("this_is_true") == 0
和ToNumber("this_is_true") == 1
。ToNumber("this_is_true")
返回NaN
,这将表达式分别简化为NaN == 0
和NaN == 1
。此时算法递归。NaN
、 0和1的类型都是Number
。跳过不适用的步骤1.a和1.b。但是,步骤1.c.i适用,因为左操作数是NaN
。算法现在返回false
(NaN
不等于任何东西,包括它本身)作为每个原始表达式的值,并回溯堆栈以完全退出递归。我的第二个示例(基于《银河系漫游指南》中对生命意义的解释)通过==
将一个对象与一个数字进行比较,返回true
:
"this_is_true" == false // false "this_is_true" == true // false
以下步骤显示了JavaScript如何使用抽象相等比较算法得出true
作为表达式的值:
Object
,右操作数的类型为Number
。表达式转换为ToPrimitive(lifeAnswer) == 42
。ToPrimitive()
调用lifeAnswer
的[[DefaultValue]]
内部方法,没有提示。根据ECMAScript 262规范中的第8.12.8节,[[DefaultValue]]
调用toString()
方法,该方法返回“42”。表达式转换为"42" == 42
,算法递归。String
,右操作数的类型为Number
。表达式转换为ToNumber("42") == 42
。ToNumber("42")
返回42,表达式转换为42 == 42
。算法递归并执行步骤1.c.iii。因为数字相同,所以返回true
,并且递归展开。对于我的最后一个示例,让我们找出为什么以下序列没有显示传递性,其中第三个比较将返回true
而不是false
:
"this_is_true" == false // false "this_is_true" == true // false
以下步骤显示了JavaScript如何使用抽象相等比较算法得出true
作为'' == 0
的值。
ToNumber('') == 0
,这转换为0 == 0
,算法递归。(规范的第9.3.1节指出StringNumericLiteral ::: [empty]的MV[数学值]为0。换句话说,空字符串的数值为0。)true
(并展开递归)。以下步骤显示了JavaScript如何使用抽象相等比较算法得出true
作为0 == '0'
的值:
0 == ToNumber('0')
,这转换为0 == 0
,算法递归。true
(并展开递归)。最后,JavaScript执行抽象相等比较算法中的步骤1.d以得出true
作为'' == '0'
的值。因为这两个字符串的长度不同(0和1),所以返回false
。
结论
你可能想知道为什么你应该费心使用==
和!=
。毕竟,之前的例子已经表明,由于类型强制转换和递归,这些运算符可能比===
和!==
运算符慢。你可能希望使用==
和!=
,因为在某些情况下===
和!==
没有优势。考虑以下示例:
"this_is_true" == false // false "this_is_true" == true // false
typeof
运算符返回一个String
值。因为String
值与另一个String
值(“object”)进行比较,所以不会发生类型强制转换,==
与===
一样高效。也许从未遇到过===
的JavaScript新手会发现这样的代码更清晰。类似地,以下代码片段不需要类型强制转换(两个操作数的类型都是Number
),因此!=
与!==
一样高效:
'' == 0 // true 0 == '0' // true '' == '0' // false
这些示例表明,==
和!=
适用于不需要强制转换的比较。当操作数类型不同时,===
和!==
是最佳选择,因为它们返回false
而不是意外值(例如,false == ""
返回true
)。如果操作数类型相同,则没有理由不使用==
和!=
。也许是时候停止害怕邪恶双胞胎了,一旦你了解了它们,它们就不那么邪恶了。
JavaScript相等和比较运算符的常见问题解答 (FAQs)
==
和===
在JavaScript中的区别是什么?在JavaScript中,==
和===
都是比较运算符。但是,它们在比较值的方式上有所不同。==
运算符(也称为松散相等运算符)在比较之前执行类型强制转换。这意味着如果你比较两种不同类型的值,JavaScript将在执行比较之前尝试将一种类型转换为另一种类型。另一方面,===
运算符(称为严格相等运算符)不执行类型强制转换。它同时比较值和类型,这意味着如果两个值类型不同,JavaScript会认为它们不相等。
===
而不是==
?通常建议在JavaScript中使用===
而不是==
,因为它提供更严格的比较,这意味着它不执行类型强制转换并检查值和类型。这可以帮助避免在比较不同类型的值时出现意外结果。例如,当使用==
时,JavaScript会认为数字0和空字符串“”相等,因为它在比较之前转换类型。但是,使用===
,它们将被认为不相等,因为它们类型不同。
JavaScript中的类型强制转换是指将值从一种数据类型自动或隐式转换为另一种数据类型。当对不同类型的操作数使用运算符或需要某种类型时,就会发生这种情况。例如,当使用松散相等运算符(==
)时,JavaScript将在进行比较之前尝试将操作数转换为通用类型。
在JavaScript中,对象按引用比较,而不是按值比较。这意味着即使两个对象具有完全相同的属性和值,它们也不被认为相等,因为它们引用内存中的不同对象。对象被认为相等的唯一情况是它们引用完全相同的对象。
==
和!=
在JavaScript中的区别是什么?==
和!=
都是JavaScript中的比较运算符。==
运算符检查两个操作数的值是否相等,必要时执行类型强制转换。另一方面,!=
运算符检查两个操作数的值是否不相等,必要时也执行类型强制转换。
===
和!==
在JavaScript中的区别是什么?===
和!==
都是JavaScript中的比较运算符。===
运算符检查两个操作数的值是否相等,同时考虑值和类型。另一方面,!==
运算符检查两个操作数的值是否不相等,同时考虑值和类型。
在JavaScript中,数组是对象,按引用比较,而不是按值比较。这意味着即使两个数组按相同的顺序包含相同的元素,它们也不被认为相等,因为它们引用内存中的不同对象。要按其内容比较两个数组,你需要分别比较每个元素。
null
和undefined
的比较?在JavaScript中,null
和undefined
被认为是松散相等(==
),因为它们都表示值的缺失。但是,它们不是严格相等(===
),因为它们的类型不同。
在JavaScript中,比较运算符具有相同的优先级级别。它们从左到右计算。但是,重要的是要注意,它们的优先级低于算术和按位运算符,但高于逻辑运算符。
是的,你可以在JavaScript中将比较运算符与字符串一起使用。比较字符串时,JavaScript使用词法(字典)顺序。但是,重要的是要注意,大写字母被认为比小写字母“小”,因为它们的ASCII值较小。
以上是不要害怕邪恶的双胞胎-Sitepoint的详细内容。更多信息请关注PHP中文网其他相关文章!