常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”
相等比较操作的性能
相等比较操作的性能
根据第一种解释(不准确的版本),=== 似乎比 == 做的事情更多,因为它还要检查值的类型。第二种解释中 == 的工作量更大一些,因为如果值的类型不同还需要进行强制类型转换。
有人觉得 == 会比 === 慢,实际上虽然强制类型转换确实要多花点时间,但仅仅是微秒级 (百万分之一秒)的差别而已。
如果进行比较的两个值类型相同,则 == 和 === 使用相同的算法,所以除了 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 节中还规定,== 在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。
字符串和数字之间的相等比较
var a = 42;var b = "42";a === b; // falsea == b; // true
因为没有强制类型转换,所以 a === b 为 false,42 和 "42" 不相等。而 a == b 是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。
ES5 规范 11.9.3.4-5 这样定义:
- 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果
- 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果
根据规范,”42” 应该被强制类型转换为数字以便进行相等比较。
其他类型和布尔类型之间的相等比较
== 最容易出错的一个地方是 true 和 false 与其他类型之间的相等比较。例如:
var a = "42";var b = true;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。例如:
var a = "42";// 不要这样用,条件判断不成立:if (a == true) {// ..}// 也不要这样用,条件判断不成立:if (a === true) {// ..}// 这样的显式用法没问题:if (a) {// ..}// 这样的显式用法更好:if (!!a) {// ..}// 这样的显式用法也很好:if (Boolean( a )) {// ..}
null 和 undefined 之间的相等比较
null 和 undefined 之间的 == 也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 规定:
- 如果
x为null,y为undefined,则结果为true - 如果
x为undefined,y为null,则结果为true
在 ==中 null 和 undefined 相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
这也就是说在 == 中 null 和 undefined 是一回事,可以相互进行隐式强制类型转换:
var a = null;var b;a == b; // truea == null; // trueb == null; // truea == false; // falseb == false; // falsea == ""; // falseb == ""; // falsea == 0; // falseb == 0; // false
null 和 undefined 之间的强制类型转换是安全可靠的。作者认为通过这种方式将 null 和 undefined 作为等价值来处理比较好。例如:
var a = doSomething();if (a == null) {// ..}
条件判断a == null仅在doSomething()返回非null和undefined时才成立,除此之外其他值都不成立,包括 0、false 和 "" 这样的假值。
对象和非对象之间的相等比较
关于对象(对象 / 函数 / 数组)和标量基本类型(字符串 / 数字 / 布尔值)之间的相等比 较,ES5 规范 11.9.3.8-9 做如下规定:
- 如果
Type(x)是字符串或数字,Type(y)是对象,则返回x == ToPrimitive(y)的结果 - 如果
Type(x)是对象,Type(y)是字符串或数字,则返回ToPromitive(x) == y的结果
例如:
var a = 42;var b = [ 42 ];a == b; // true
[ 42 ] 首先调用 ToPromitive 抽象操作,返回 "42",变成 "42" == 42,然后 又变成 42 == 42,最后二者相等。
之前说过“拆封”,即“打开”封装对象(如new String(“abc”)),返回其中的基本数据类型值(``"abc"``)。== 中的 ToPromitive 强制类型转换也会发生这样的情况:
var a = "abc";var b = Object( a ); // 和new String( a )一样a === b; // falsea == b; // true
a == b结果为true,因为b通过ToPromitive进行强制类型转换(也称为“拆封”,英文为 unboxed 或者 unwrapped),并返回标量基本类型值 "abc",与 a 相等。
但有一些值不这样,原因是 == 算法中其他优先级更高的规则。例如:
var a = null;var b = Object( a ); // 和Object()一样a == b; // falsevar c = undefined;var d = Object( c ); // 和Object()一样c == d; // falsevar e = NaN;var f = Object( e ); // 和new Number( e )一样e == f; // false
因为没有对应的封装对象,所以 null 和 undefined 不能够被封装(boxed),Object(null) 和 Object() 均返回一个常规对象。NaN能够被封装为数字封装对象,但拆封之后NaN == NaN返回false,因为NaN不等于NaN
比较少见的情况
返回其他数字
Number.prototype.valueOf = function() {return 3;};new Number( 2 ) == 3; // true
2 == 3不会有这种问题,因为2和3都是数字基本类型值,不会调用 Number.prototype.valueOf()方法。而 Number(2) 涉及 ToPrimitive 强制类型转换,因此会调用 valueOf()。
如何让a == 2 && a == 3成立?
var i = 2;Number.prototype.valueOf = function() {return i++;};var a = new Number( 42 );if (a == 2 && a == 3) {console.log( "Yep, this happened." );}
假值的相等比较
下面分别列出了常规和非常规的情况:
"0" == null; // false"0" == undefined; // false"0" == false; // true"0" == NaN; // false"0" == 0; // true"0" == ""; // falsefalse == null; // falsefalse == undefined; // falsefalse == NaN; // falsefalse == 0; // truefalse == ""; // truefalse == []; // truefalse == {}; // false"" == null; // false"" == undefined; // false"" == NaN; // false"" == 0; // true"" == []; // true"" == {}; // false0 == null; // false0 == undefined; // false0 == NaN; // false0 == []; // true0 == {}; // false
极端情况
[] == ![] // true
根据 ToBoolean 规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以[] == ![]变成了[] == false。
2 == [2]; // true"" == [null]; // true
== 右边的值 [2] 和 [null] 会进行 ToPrimitive 强制类型转换, 以便能够和左边的基本类型值(2 和 “”)进行比较。因为数组的 valueOf() 返回数组本身, 所以强制类型转换过程中数组会进行字符串化。
第一行中的 [2] 会转换为 "2",然后通过 ToNumber 转换为 2。第二行中的 [null] 会直接转换为 ""。
所以最后的结果就是 2 == 2 和 "" == ""。
0 == "\n"; // true
""、"\n"(或者 " " 等其他空格组合)等空字符串被 ToNumber 强制类型转换为 0。
安全运用隐式强制类型转换
以下两个原则可以让我们有效地避免出错:
- 如果两边的值中有
true或者false,千万不要使用== - 如果两边的值中有
[]、""或者0,尽量不要使用==
