常见的误区是“== 检查值是否相等,=== 检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许。”
相等比较操作的性能
相等比较操作的性能
根据第一种解释(不准确的版本),=== 似乎比 == 做的事情更多,因为它还要检查值的类型。第二种解释中 == 的工作量更大一些,因为如果值的类型不同还需要进行强制类型转换。
有人觉得 == 会比 === 慢,实际上虽然强制类型转换确实要多花点时间,但仅仅是微秒级 (百万分之一秒)的差别而已。
如果进行比较的两个值类型相同,则 == 和 === 使用相同的算法,所以除了 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; // false
a == 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; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 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; // false
a == b; // true
a == b
结果为true
,因为b
通过ToPromitive
进行强制类型转换(也称为“拆封”,英文为 unboxed 或者 unwrapped),并返回标量基本类型值 "abc"
,与 a
相等。
但有一些值不这样,原因是 == 算法中其他优先级更高的规则。例如:
var a = null;
var b = Object( a ); // 和Object()一样
a == b; // false
var c = undefined;
var d = Object( c ); // 和Object()一样
c == d; // false
var 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" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true
false == ""; // true
false == []; // true
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true
"" == []; // true
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true
0 == {}; // 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
,尽量不要使用==