常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。
相等比较操作的性能

相等比较操作的性能

根据第一种解释(不准确的版本),=== 似乎比 == 做的事情更多,因为它还要检查值的类型。第二种解释中 == 的工作量更大一些,因为如果值的类型不同还需要进行强制类型转换。
有人觉得 == 会比 === 慢,实际上虽然强制类型转换确实要多花点时间,但仅仅是微秒级 (百万分之一秒)的差别而已。
如果进行比较的两个值类型相同,则 == 和 === 使用相同的算法,所以除了 js 引擎 实现上的细微差别之外,它们之间并没有什么不同。
如果两个值的类型不同,就需要考虑有没有强制类型转换的必要,有就用 ==,没有就用 ===,不用在乎性能。
== 和 === 都会检查操作数的类型。区别在于操作数类型不同时它们的处理方式不同。

抽象相等

ES5 规范 11.9.3 节的“抽象相等比较算法”定义了 == 运算符的行为。该算法简单而又全面,涵盖了所有可能出现的类型组合,以及它们进行强制类型转换的方式。
其中第一段(11.9.3.1)规定如果两个值的类型相同,就仅比较它们是否相等。例如,42 等于 42,”abc” 等于 “abc”。
有几个非常规的情况需要注意:

  • NaN 不等于 NaN
  • +0 等于 -0

11.9.3.1 的最后定义了对象(包括函数和数组)的宽松相等 ==。两个对象指向同一个值时即视为相等,不发生强制类型转换。
=== 的定义和 11.9.3.1 一样,包括对象的情况。实际上在比较两个对象的时 候,===== 的工作原理是一样的。
11.9.3 节中还规定,== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。

字符串和数字之间的相等比较

  1. var a = 42;
  2. var b = "42";
  3. a === b; // false
  4. a == b; // true

因为没有强制类型转换,所以 a === bfalse42"42" 不相等。而 a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。
ES5 规范 11.9.3.4-5 这样定义:

  • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
  • 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果

根据规范,”42” 应该被强制类型转换为数字以便进行相等比较。

其他类型和布尔类型之间的相等比较

== 最容易出错的一个地方是 truefalse 与其他类型之间的相等比较。例如:

  1. var a = "42";
  2. var b = true;
  3. a == b; // false

为什么a不等于b呢?
规范 11.9.3.6-7 是这样说的:

  • 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果
  • 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。

那么上述例子可以解释为:Type(y)是布尔值,所以ToNumber(y)将强制类型转换为1,然后"42" == 1再变成
42 == 1,结果为 false。也就是说字符串 "42" 既不等于 true,也不等于 false
所以,建议无论什么情况下都不要使用 == true== false
这里说的只是===== true=== false不允许强制类型转换,所以并不涉及 ToNumber。例如:

  1. var a = "42";
  2. // 不要这样用,条件判断不成立:
  3. if (a == true) {
  4. // ..
  5. }
  6. // 也不要这样用,条件判断不成立:
  7. if (a === true) {
  8. // ..
  9. }
  10. // 这样的显式用法没问题:
  11. if (a) {
  12. // ..
  13. }
  14. // 这样的显式用法更好:
  15. if (!!a) {
  16. // ..
  17. }
  18. // 这样的显式用法也很好:
  19. if (Boolean( a )) {
  20. // ..
  21. }

null 和 undefined 之间的相等比较

null 和 undefined 之间的 == 也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 规定:

  • 如果 xnullyundefined,则结果为 true
  • 如果 xundefinedynull,则结果为 true

==nullundefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
这也就是说在 ==nullundefined 是一回事,可以相互进行隐式强制类型转换:

  1. var a = null;
  2. var b;
  3. a == b; // true
  4. a == null; // true
  5. b == null; // true
  6. a == false; // false
  7. b == false; // false
  8. a == ""; // false
  9. b == ""; // false
  10. a == 0; // false
  11. b == 0; // false

nullundefined 之间的强制类型转换是安全可靠的。作者认为通过这种方式将 nullundefined 作为等价值来处理比较好。例如:

  1. var a = doSomething();
  2. if (a == null) {
  3. // ..
  4. }

条件判断a == null仅在doSomething()返回非nullundefined时才成立,除此之外其他值都不成立,包括 0false"" 这样的假值。

对象和非对象之间的相等比较

关于对象(对象 / 函数 / 数组)和标量基本类型(字符串 / 数字 / 布尔值)之间的相等比 较,ES5 规范 11.9.3.8-9 做如下规定:

  • 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果
  • 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPromitive(x) == y 的结果

例如:

  1. var a = 42;
  2. var b = [ 42 ];
  3. a == b; // true

[ 42 ] 首先调用 ToPromitive 抽象操作,返回 "42",变成 "42" == 42,然后 又变成 42 == 42,最后二者相等。
之前说过“拆封”,即“打开”封装对象(如new String(“abc”)),返回其中的基本数据类型值(``"abc"``)== 中的 ToPromitive 强制类型转换也会发生这样的情况:

  1. var a = "abc";
  2. var b = Object( a ); // 和new String( a )一样
  3. a === b; // false
  4. a == b; // true

a == b结果为true,因为b通过ToPromitive进行强制类型转换(也称为“拆封”,英文为 unboxed 或者 unwrapped),并返回标量基本类型值 "abc",与 a 相等。
但有一些值不这样,原因是 == 算法中其他优先级更高的规则。例如:

  1. var a = null;
  2. var b = Object( a ); // 和Object()一样
  3. a == b; // false
  4. var c = undefined;
  5. var d = Object( c ); // 和Object()一样
  6. c == d; // false
  7. var e = NaN;
  8. var f = Object( e ); // 和new Number( e )一样
  9. e == f; // false

因为没有对应的封装对象,所以 nullundefined 不能够被封装(boxed),Object(null)Object() 均返回一个常规对象。
NaN能够被封装为数字封装对象,但拆封之后NaN == NaN返回false,因为NaN不等于NaN

比较少见的情况

返回其他数字

  1. Number.prototype.valueOf = function() {
  2. return 3;
  3. };
  4. new Number( 2 ) == 3; // true

2 == 3不会有这种问题,因为23都是数字基本类型值,不会调用 Number.prototype.valueOf()方法。而 Number(2) 涉及 ToPrimitive 强制类型转换,因此会调用 valueOf()
如何让a == 2 && a == 3成立?

  1. var i = 2;
  2. Number.prototype.valueOf = function() {
  3. return i++;
  4. };
  5. var a = new Number( 42 );
  6. if (a == 2 && a == 3) {
  7. console.log( "Yep, this happened." );
  8. }

假值的相等比较

下面分别列出了常规和非常规的情况:

  1. "0" == null; // false
  2. "0" == undefined; // false
  3. "0" == false; // true
  4. "0" == NaN; // false
  5. "0" == 0; // true
  6. "0" == ""; // false
  7. false == null; // false
  8. false == undefined; // false
  9. false == NaN; // false
  10. false == 0; // true
  11. false == ""; // true
  12. false == []; // true
  13. false == {}; // false
  14. "" == null; // false
  15. "" == undefined; // false
  16. "" == NaN; // false
  17. "" == 0; // true
  18. "" == []; // true
  19. "" == {}; // false
  20. 0 == null; // false
  21. 0 == undefined; // false
  22. 0 == NaN; // false
  23. 0 == []; // true
  24. 0 == {}; // false

极端情况

  1. [] == ![] // true

根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以[] == ![]变成了[] == false

  1. 2 == [2]; // true
  2. "" == [null]; // true

== 右边的值 [2][null] 会进行 ToPrimitive 强制类型转换, 以便能够和左边的基本类型值(2 和 “”)进行比较。因为数组的 valueOf() 返回数组本身, 所以强制类型转换过程中数组会进行字符串化。
第一行中的 [2] 会转换为 "2",然后通过 ToNumber 转换为 2。第二行中的 [null] 会直接转换为 ""
所以最后的结果就是 2 == 2"" == ""

  1. 0 == "\n"; // true

"""\n"(或者 " " 等其他空格组合)等空字符串被 ToNumber 强制类型转换为 0

安全运用隐式强制类型转换

以下两个原则可以让我们有效地避免出错:

  • 如果两边的值中有 true 或者 false,千万不要使用 ==
  • 如果两边的值中有 []"" 或者 0,尽量不要使用 ==