首页 > web前端 > js教程 > 不要害怕邪恶的双胞胎-Sitepoint

不要害怕邪恶的双胞胎-Sitepoint

Jennifer Aniston
发布: 2025-02-22 08:58:11
原创
895 人浏览过

Don't Fear the Evil Twins - SitePoint

JavaScript开发者Douglas Crockford曾将JavaScript的==!=运算符称为应该避免的“邪恶双胞胎”。然而,一旦你理解了它们,这些运算符并没有那么糟糕,实际上可能很有用。本文将探讨==!=,解释它们的工作原理,并帮助你更好地了解它们。

关键要点

  • 理解基础知识: JavaScript中的==!=运算符并非天生邪恶;它们在比较不同类型的值时执行类型强制转换,这既有用又棘手。
  • 了解何时使用哪个: 使用===!==进行直接的类型和值比较,无需强制转换,这更清晰,通常建议避免意外结果。当需要类型强制转换或比较类型可能动态变化的值时,使用==!=
  • 学习强制转换规则: 熟悉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节介绍了相等运算符。==!=运算符出现在语法产生式EqualityExpressionEqualityExpressionNoIn中。(与第一个产生式不同,第二个产生式避免了in运算符。)让我们检查一下下面显示的EqualityExpression产生式。

'' == 0   // true
0 == '0' // true
'' == '0' // false
登录后复制
登录后复制

根据此产生式,相等表达式要么是关系表达式,要么是通过==等于关系表达式的相等表达式,要么是通过!=不等于关系表达式的相等表达式,等等。(我忽略了===!==,它们与本文无关。)第11.9.1节提供了关于==如何工作的以下信息:

产生式EqualityExpression : EqualityExpression == RelationalExpression的计算如下:

  1. lref为计算EqualityExpression的结果。
  2. lvalGetValue(lref)
  3. rref为计算RelationalExpression的结果。
  4. rvalGetValue(rref)
  5. 返回执行抽象相等比较rval == lval的结果。(参见11.9.3。)

第11.9.2节提供了关于!=如何工作的类似信息:

产生式EqualityExpression : EqualityExpression != RelationalExpression的计算如下:

  1. lref为计算EqualityExpression的结果。
  2. lvalGetValue(lref)
  3. rref为计算RelationalExpression的结果。
  4. rvalGetValue(rref)
  5. r为执行抽象相等比较rval != lval的结果。(参见11.9.3。)
  6. 如果rtrue,则返回false。否则,返回true

lrefrref==!=运算符左右两侧的引用。每个引用都传递给GetValue()内部函数以返回相应的值。==!=如何工作的核心由抽象相等比较算法指定,该算法在第11.9.3节中给出:

比较x == y,其中xy是值,产生truefalse。此类比较如下进行:

  1. 如果Type(x)Type(y)相同,则
    1. 如果Type(x)Undefined,则返回true
    2. 如果Type(x)Null,则返回true
    3. 如果Type(x)Number,则
      1. 如果xNaN,则返回false
      2. 如果yNaN,则返回false
      3. 如果xy是相同的数字值,则返回true
      4. 如果x是 0且y是-0,则返回true
      5. 如果x是-0且y是 0,则返回true
      6. 返回false
    4. 如果Type(x)String,则如果xy是完全相同的字符序列(长度相同且对应位置的字符相同),则返回true。否则,返回false
    5. 如果Type(x)Boolean,则如果xy都是true或都是false,则返回true。否则,返回false
    6. 如果xy引用同一个对象,则返回true。否则,返回false
  2. 如果xnullyundefined,则返回true
  3. 如果xundefinedynull,则返回true
  4. 如果Type(x)NumberType(y)String,则返回比较x == ToNumber(y)的结果。
  5. 如果Type(x)StringType(y)Number,则返回比较ToNumber(x) == y的结果。
  6. 如果Type(x)Boolean,则返回比较ToNumber(x) == y的结果。
  7. 如果Type(y)Boolean,则返回比较x == ToNumber(y)的结果。
  8. 如果Type(x)StringNumberType(y)Object,则返回比较x == ToPrimitive(y)的结果。
  9. 如果Type(x)ObjectType(y)StringNumber,则返回比较ToPrimitive(x) == y的结果。
  10. 返回false

步骤1在此算法中执行时操作数类型相同。它表明undefined等于undefinednull等于null。它还表明没有任何东西等于NaN(非数字),两个相同的数值相等, 0等于-0,两个具有相同长度和字符序列的字符串相等,true等于truefalse等于false,以及对同一对象的两个引用相等。步骤2和3显示了为什么null != undefined返回false。JavaScript认为这些值相同。从步骤4开始,算法变得有趣。此步骤侧重于NumberString值之间的相等性。当第一个操作数是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,则应用以下步骤:
    1. primValueToPrimitive(输入参数, 提示Number)
    2. 返回ToNumber(primValue)

ToPrimitive()函数接受一个输入参数和一个可选的PreferredType参数。输入参数转换为非对象类型。如果对象能够转换为多个原始类型,则ToPrimitive()使用可选的PreferredType提示来偏向首选类型。转换如下进行:

  1. 如果输入参数是Undefined,则返回输入参数(Undefined)——没有转换。
  2. 如果输入参数是Null,则返回输入参数(Null)——没有转换。
  3. 如果输入参数的类型是Boolean,则返回输入参数——没有转换。
  4. 如果输入参数的类型是Number,则返回输入参数——没有转换。
  5. 如果输入参数的类型是String,则返回输入参数——没有转换。
  6. 如果输入参数的类型是Object,则返回与输入参数相对应的默认值。通过调用对象的[[DefaultValue]]内部方法并传递可选的PreferredType提示来检索对象的默认值。[[DefaultValue]]的行为在第8.12.8节中为所有原生ECMAScript对象定义。

本节介绍了相当多的理论。在下一节中,我们将通过提供涉及==!=的各种表达式并逐步完成算法步骤来评估它们,从而转向实践。

了解邪恶双胞胎

现在我们已经根据ECMAScript规范了解了==!=的工作原理,让我们通过探索涉及这些运算符的各种表达式来充分利用这些知识。我们将逐步介绍如何评估这些表达式,并找出它们为什么是truefalse。对于我的第一个示例,请考虑文章开头附近介绍的以下表达式对:

"this_is_true" == false // false
"this_is_true" == true  // false
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

按照抽象相等比较算法,按照以下步骤评估这些表达式:

  1. 跳过步骤1,因为类型不同:typeof "this_is_true"返回“string”,而typeof falsetypeof true返回“boolean”。
  2. 跳过不适用的步骤2到6,因为它们与操作数类型不匹配。但是,步骤7适用,因为右参数的类型为Boolean。表达式转换为"this_is_true" == ToNumber(false)"this_is_true" == ToNumber(true)
  3. ToNumber(false)返回 0,ToNumber(true)返回1,这将表达式分别简化为"this_is_true" == 0"this_is_true" == 1。此时算法递归。
  4. 跳过不适用的步骤1到4,因为它们与操作数类型不匹配。但是,步骤5适用,因为左操作数的类型为String,右操作数的类型为Number。表达式转换为ToNumber("this_is_true") == 0ToNumber("this_is_true") == 1
  5. ToNumber("this_is_true")返回NaN,这将表达式分别简化为NaN == 0NaN == 1。此时算法递归。
  6. 进入步骤1,因为NaN、 0和1的类型都是Number。跳过不适用的步骤1.a和1.b。但是,步骤1.c.i适用,因为左操作数是NaN。算法现在返回falseNaN不等于任何东西,包括它本身)作为每个原始表达式的值,并回溯堆栈以完全退出递归。

我的第二个示例(基于《银河系漫游指南》中对生命意义的解释)通过==将一个对象与一个数字进行比较,返回true

"this_is_true" == false // false
"this_is_true" == true  // false
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

以下步骤显示了JavaScript如何使用抽象相等比较算法得出true作为表达式的值:

  1. 跳过不适用的步骤1到8,因为它们与操作数类型不匹配。但是,步骤9适用,因为左操作数的类型为Object,右操作数的类型为Number。表达式转换为ToPrimitive(lifeAnswer) == 42
  2. ToPrimitive()调用lifeAnswer[[DefaultValue]]内部方法,没有提示。根据ECMAScript 262规范中的第8.12.8节,[[DefaultValue]]调用toString()方法,该方法返回“42”。表达式转换为"42" == 42,算法递归。
  3. 跳过不适用的步骤1到4,因为它们与操作数类型不匹配。但是,步骤5适用,因为左操作数的类型为String,右操作数的类型为Number。表达式转换为ToNumber("42") == 42
  4. ToNumber("42")返回42,表达式转换为42 == 42。算法递归并执行步骤1.c.iii。因为数字相同,所以返回true,并且递归展开。

对于我的最后一个示例,让我们找出为什么以下序列没有显示传递性,其中第三个比较将返回true而不是false

"this_is_true" == false // false
"this_is_true" == true  // false
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制

以下步骤显示了JavaScript如何使用抽象相等比较算法得出true作为'' == 0的值。

  1. 执行步骤5,导致ToNumber('') == 0,这转换为0 == 0,算法递归。(规范的第9.3.1节指出StringNumericLiteral ::: [empty]的MV[数学值]为0。换句话说,空字符串的数值为0。)
  2. 执行步骤1.c.iii,它将0与0进行比较并返回true(并展开递归)。

以下步骤显示了JavaScript如何使用抽象相等比较算法得出true作为0 == '0'的值:

  1. 执行步骤4,导致0 == ToNumber('0'),这转换为0 == 0,算法递归。
  2. 执行步骤1.c.iii,它将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中使用===而不是==,因为它提供更严格的比较,这意味着它不执行类型强制转换并检查值和类型。这可以帮助避免在比较不同类型的值时出现意外结果。例如,当使用==时,JavaScript会认为数字0和空字符串“”相等,因为它在比较之前转换类型。但是,使用===,它们将被认为不相等,因为它们类型不同。

JavaScript中的类型强制转换是什么?

JavaScript中的类型强制转换是指将值从一种数据类型自动或隐式转换为另一种数据类型。当对不同类型的操作数使用运算符或需要某种类型时,就会发生这种情况。例如,当使用松散相等运算符(==)时,JavaScript将在进行比较之前尝试将操作数转换为通用类型。

JavaScript如何处理对象的比较?

在JavaScript中,对象按引用比较,而不是按值比较。这意味着即使两个对象具有完全相同的属性和值,它们也不被认为相等,因为它们引用内存中的不同对象。对象被认为相等的唯一情况是它们引用完全相同的对象。

==!=在JavaScript中的区别是什么?

==!=都是JavaScript中的比较运算符。==运算符检查两个操作数的值是否相等,必要时执行类型强制转换。另一方面,!=运算符检查两个操作数的值是否不相等,必要时也执行类型强制转换。

===!==在JavaScript中的区别是什么?

===!==都是JavaScript中的比较运算符。===运算符检查两个操作数的值是否相等,同时考虑值和类型。另一方面,!==运算符检查两个操作数的值是否不相等,同时考虑值和类型。

如何在JavaScript中比较两个数组?

在JavaScript中,数组是对象,按引用比较,而不是按值比较。这意味着即使两个数组按相同的顺序包含相同的元素,它们也不被认为相等,因为它们引用内存中的不同对象。要按其内容比较两个数组,你需要分别比较每个元素。

JavaScript如何处理nullundefined的比较?

在JavaScript中,nullundefined被认为是松散相等(==),因为它们都表示值的缺失。但是,它们不是严格相等(===),因为它们的类型不同。

JavaScript中比较运算符的优先级顺序是什么?

在JavaScript中,比较运算符具有相同的优先级级别。它们从左到右计算。但是,重要的是要注意,它们的优先级低于算术和按位运算符,但高于逻辑运算符。

我可以在JavaScript中将比较运算符与字符串一起使用吗?

是的,你可以在JavaScript中将比较运算符与字符串一起使用。比较字符串时,JavaScript使用词法(字典)顺序。但是,重要的是要注意,大写字母被认为比小写字母“小”,因为它们的ASCII值较小。

以上是不要害怕邪恶的双胞胎-Sitepoint的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板