在 JavaScript 中,==
和 ===
是两种常用的比较运算符,它们用于判断两个值是否相等,但它们的行为有显著不同。了解它们的区别非常重要,以便避免不必要的错误和困惑。
==
看似做的更多,因为它需要进行强制类型转换,但实际上这种转换的开销非常小,只是微秒级的差异(百万分之一秒)。因此,通常我们并不需要为性能担忧。
如果两个值的类型不同,且需要强制类型转换,使用 ==
是合适的。如果类型相同,使用 ===
会更直观、安全,且不涉及类型转换。
抽象相等
ES5 规范 11.9.3 节的 抽象相等比较算法
定义了 ==
运算符的行为,在使用 ==
比较的时候,当使用 ==
对 x 和 y 进行比较时,会返回 true
或者 false
的值,它们主要遵循以下的规则:
-
如果
x
和y
的类型相同:-
如果
x
是undefined
,返回true
; -
如果
x
是null
,返回true
; -
如果
x
是Number
类型,则:-
如果
x
是NaN
,则返回false
; -
如果
y
是NaN
,则返回false
; -
如果
x
和y
相同,则返回true
; -
如果
x
是-0
和y
是+0
,则返回true
; -
如果
y
是-0
和x
是+0
,则返回true
; -
其他情况返回
false
;
-
-
如果
x
是string
类型,并且x
和y
的完全相同的值(长度相同,对应位置的字符相同),则返回true
; -
如果
x
的Boolean
类型,并且x
和y
都是true
或者false
,则返回true
,否则返回false
; -
如果
x
和y
引用同一个对象,则返回true
,否则返回false
;
-
-
如果
x
为null
且y
为undefined
,则返回true
; -
如果
y
为null
且x
为undefined
,则返回true
; -
如果
x
是number
类型且y
是string
类型,则返回x == ToNumber(y)
的比较结果; -
如果
y
是number
类型且x
是string
类型,则返回y == ToNumber(x)
的比较结果; -
如果
x
是boolean
类型,返回ToNumber(x) == y
的比较结果; -
如果
y
是boolean
类型,返回ToNumber(y) == x
的比较结果; -
如果
x
是string
类型或者number
类型,并且y
是object
类型,返回x == ToPrimitive(y)
的比较结果; -
如果
y
是string
类型或者number
类型,并且x
是object
类型,返回y == ToPrimitive(x)
的比较结果; -
否则返回
false
;
抽象相等的这些规则正是隐式强制类型转换不受人喜爱的原因,但是认真一看规则,其实简单明了。
字符串和数字之间的相等比较
const a = 77;
const b = '77';
console.log(a === b); // false
console.log(a == b); // true
因为没强制类型转换,所以 a === b
为 false
,77
和 "77"
不相等。
而 a == b
是宽松相等,即如果两个值的类型不同,则对其中之一或者两者都进行强制类型转换,具体怎么转换的,请看定义,它们是这样的:
-
如果
x
是number
类型且y
是string
类型,则返回x == ToNumber(y)
的比较结果; -
如果
y
是number
类型且x
是string
类型,则返回y == ToNumber(x)
的比较结果;
也就是说, a == b
在代码中实际上是这样的行为:
const a = 77;
const b = '77';
console.log(a === Number(b)); // true
其他类型和布尔值之间的相等比较
==
最容易出错的是一个地方是 true
和 false
于其他类型之间的相等比较,例如:
const a = '77';
const b = true;
console.log(a == b); // false
我们都知道 "77"
是一个真值,为什么 ==
的结果不是 true
呢?因为规范是这样定义的:
-
如果
x
是boolean
类型,返回ToNumber(x) == y
的比较结果; -
如果
y
是boolean
类型,返回ToNumber(y) == x
的比较结果;
首先 b
是 boolean
类型,所以 ToNumber(b)
将 b
的类型强制转换为 1
,变成 1 == '77'
,二者的类型仍然不同,"77"
根据规则被强制类型转换为 77
,最后变成 1 == 77
,所以结果输出为 false
;
null 和 undefined 之间的相等比较
null
和 undefined
之间的 ==
也设计隐式强制类型转换,ES5规范
是这样规定的:
- 如果
x
为null
且y
为undefined
,则返回true
; - 如果
y
为null
且x
为undefined
,则返回true
;
在 ==
中 null
和 undefined
相等(它们也与其自身相等),除此之外其他值都不和它们两个相等。
这也就是说,在==
中 null
和 undefined
是一回事,可以相互进行隐式强制类型转换:
var a = null;
var b = undefined;
console.log(a == b); // true
console.log(a == null); // true
console.log(b == null); // true
console.log(a == false); // false
console.log(b == false); // false
console.log(a == ''); // false
console.log(b == ''); // false
console.log(a == 0); // false
console.log(b == 0); // false
null
he undefined
之间的强制类型转换是安全可靠的,上例中除 null
和 undefined
以外的其他值均为无法返回 true
的结果。
对象和非对象之间的相等比较
对于对象和基本类型之间的相等比较,ES5规范
是遵循这样的规则:
- 如果
x
是string
类型或者number
类型,并且y
是object
类型,返回x == ToPrimitive(y)
的比较结果; - 如果
y
是string
类型或者number
类型,并且x
是object
类型,返回y == ToPrimitive(x)
的比较结果;
- 例如:
var a = 77;
var b = [77];
console.log(a == b); // true
[77]
首先调用了 ToPrimitive
抽象操作,返回 "77"
,变成 "77" == 77
,然后又变成 77 == 77
,最后二者相等。其中代码的转变过程是以下的形式:
var a = 77;
var b = [77];
console.log(a === Number(b.toString())); // true
我们再来看一个 string
类型和 object
类型的例子:
var a = 'abc';
var b = Object(a);
console.log(a === b); // false
console.log(a == b); // b
a == b
结果为 true
,因为 b
通过 TOPrimitive
进行强制类型转换(拆封),并返回基本数据类型值 "abc"
,与 a
相等。
但是规则总有特例,原因是 ==
算法中其他优先级更高的原则。例如:
var foo = null;
var bar = Object(foo);
console.log(foo == bar); // false
var a = undefined;
var b = Object(a);
console.log(a == b); // false
var c = NaN;
var d = Object(NaN);
console.log(c == d); // false
因为没有对应的封装对象,所以 null
和 undefined
不能够被封装和 Object()
均返回一个空对象,你也可以理解成空对象调用 toString()
方法返回的值是 "[object Object]"
,不等于 null
和 undefined
。
NaN
能够被封装为数字封装对象,但拆封之后 NaN == NaN
返回 false,因为 NaN
不等于 NaN
。
希望你永远不会用到
在上面的讲解中,我们已经介绍了 ==
中的隐式强制类型转换,现在来看一下那些需要特别注意和避免的比较少见的情况。
返回其他的数字
首先来看看更改内置原生模型会导致哪些奇怪的结果:
Number.prototype.valueOf = function () {
return 3;
};
console.log(new Number(77) == 3); // true
2 == 3
不会有这样的问题,因为 2
和 3
都是数字基本类型值,不会调用 Number.prototype.valueOf()
方法。而 Number(2)
涉及 ToPrimitive
强制类型转换,因此会调用 valueOf()
。
再来看一种情况,这个你可能面试题里经常看到,a == 2 && a == 5
在什么情况下为 true
?
if (a == 2 && a == 3) {
// ...
}
你也许觉得不可能,因为 a
不会同时等于 2
和 3
。但 同时似乎说的不对,因为 a == 2
在 a == 3
之前执行。如果让 a..valueOf()
每次调用都产生副作用,比如第一次返回 2
,第二次返回 3
,就会出现这样的情况。这实现起来很简单:
const a = {
value: 1,
};
a.valueOf = function () {
return this.value++;
};
if (a == 1 && a == 2 && a == 3) {
console.log("嗨,没想到吧,意不意外,惊不惊喜"); // 这里正常输出了
}
假值的相等比较
==
中的隐式强制类型转换最为让人不满的地方是假值的相等比较下面分别列出了常规和非常规的情况:
console.log('0' == null); // false
console.log('0' == undefined); // false
console.log('0' == false); // true
console.log('0' == NaN); // false
console.log('0' == 0); // true
console.log('0' == ''); // false
console.log(false == null); // false
console.log(false == undefined); // false
console.log(false == NaN); // false
console.log(false == 0); // true
console.log(false == ''); // true
console.log(false == []); // true
console.log(false == {}); // false
console.log('' == null); // false
console.log('' == undefined); // false
console.log('' == NaN); // false
console.log('' == 0); // true
console.log('' == []); // true
console.log('' == {}); // false
console.log(0 == null); // false
console.log(0 == undefined); // false
console.log(0 == NaN); // false
console.log(0 == []); // true
console.log(0 == {}); // false
接下来我们挑一些有代表性的例子来具体讲解一下:
-
其中这些例子中最好辨认的是在
==
比较中,NaN
与任何值相比都是false
,NaN
也是; -
在
"0" == false
中会对false
先转换成数字类型,即0
,变成了"0" == 0
,再对"0"
转换,最终变成了0 == 0
,所以输出为true
; -
在
false == []
中,会对[]
进行ToPrimitive
类型转换,转换成原始类型""
,这时候变成了false == ""
,所以输出结果为true
,至于为什么是true
,我想你应该懂了吧
接下来还有一些例子:
console.log([] == ![]); // true
console.log('' == [null]); // true
console.log('' == [undefined]); // true
喔喔喔,这是什么,怎么都输出了 true
,它们到底都干了啥?
在第一个中, !
运算符对 []
做了取反操作,因为 []
为真值,而 ![]
也就变成了 false
了,所以 [] == ![]
变成了 [] == false
,前面我们有讲过这个比较的结果,所以最后的结果也很正常了。
在第二、三个例子中,[null].toString()
最后返回 ""
,而难懂的是 String(null)
返回的是 "null"
,而 String([Null])
去返回的是 ""
,这就是难懂的地方,这也是 js
令人深入理解的原因。
所以 ==
和 ===
选择哪一个取决于是否允许在相等比较中发生强制类型转换。
好了,本篇的内容讲解到此结束了,有什么不了解的可以在评论区留下你的疑问。
参考文章
-
书籍 `你不知道的 JavaScript.