Serenader

Learning by sharing

== VS === ? The Evil or the Angel ?

如果你是一个前端开发人员的话,那么难免会遇上这样的情况
console.log(0 == false); // true
console.log('' == false); // true
console.log([] == false); // true
console.log(0 === false); //false
console.log('' === false); //false
console.log([] === false); // false
不熟悉 JavaScript 的开发者也许会问:== 为啥既然有了 == 了,还要 === 干啥?这两者有什么区别?==

== 顾名思义,是相等运算符。而 === 是全等,也叫严格模式下的相等。上面的例子其实可以看出这两者有挺大的区别的。
在 JavaScript 中,== 运算符会在进行比较的时候进行一次隐式类型转换,这个转换是开发人员看不到的。比较的结果也会像前面的例子那样神奇。而 === 运算符则是进行严格的对比,如果两个变量的类型不一致或者类型一致,但是值不同的话,均会返回 false 。== 运算符其实在很多地方会有令人意想不到的结果,如果你觉得上面的例子不是很难懂的话,那么来看看下面的这个例子:
if ([0]) {
	console.log([0] == true); // false
    console.log([0] == false); // true}

if ('something') {
    console.log('something' == true); // false
    console.log('something' == false); // false}
嗯,你是不是也开始觉得神奇了?
为啥 if ([0]) 是成立的,而 [0] == true 是 false ?同样的道理,为啥 if ('string')是成立的,而 'string' == true 是 false?最奇怪的是 'string' == false 居然也是 false
是的,这个 == 运算符看似很奇怪,运算的结果经常会出乎人意料,所以,现在也有很多 JavaScript 大神都在呼吁不要使用 == 运算符进行比较,而应该使用 === 进行比较。因为相对于 == 来说,===运算符的运算结果则靠谱多了,或者说,其结果是可以清楚的被预知的。
在我刚开始学 JavaScript 的时候,看到了不少关于这方面的说法。于是乎一开始我也是对这种说法深信不疑,而且对 == 敬而远之。总之,凡是进行比较的运算,我都是会使用 === 而不是使用 == 。甚至会跟身边的小伙伴们说,使用 == 进行比较是一种不合格的写法。
虽然说使用 === 运算符在日常开发中运作得很好,也没有什么值得注意的。但是,对于 == 运算符来说,我是一点都不了解。只知道进行比较之前会进行隐式类型转换。我也曾经疑惑过,我也曾经想弄清楚这里面的奥秘,深入去掌握它,但是终究是太懒,后来也就没有去了解它的欲望了。
直到后来看到了一篇文章,才发现,原来当初的想法错的多离谱,而且,原来 JavaScript 中的 ==运算符其实一点都不难理解
好吧,扯了这么多,下面开始进入正题。

在上面的例子中,有两个地方值得研究。

第一个是求真假值:

// if 表达式中的条件
if (expression) {
	// code goes here
}
// 或者三元表达式
var something = a ? 'a' : 'b';

第二个则是 == 相等比较:

console.log(undefined == null);
console.log([0] == '');
好在,如今的浏览器在处理上面的这些操作时都已经达到了一个标准了。

求真假值

在求真假值的时候,你只需要记住:
  1. 如果需要判断的变量为对象时,全部返回 true
  2. 如果变量是字符串,而且不为空字符串时,返回 true 。
  3. 如果变量是数字,而且不为 +0 或者 -0 或者 NaN 时,返回 true
  4. 如果变量是布尔值,则返回它本身的值。(不会进行转换)
  5. 如果变量是 undefined 或者 null 时,则返回 false 。
有了这些规则之后,那么再看回原来的例子就很好理解了。
// [0] 是一个数组,而数组本质上是一个对象,因此为 `true` 。
if ([0]) {

}

// 'something' 是一个非空的字符串,因此返回 `true`。
if ('something') {

}
真假值的判断很容易掌握和理解。不过尽管规则很容易理解,但是还是有些地方容易出错,不信来看看:
// copied from https://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/
var trutheyTester = function(expr) {
    return expr ? "truthey" : "falsey"; 
}
 
trutheyTester({}); //truthey (an object is always true)
 
trutheyTester(false); //falseytrutheyTester(new Boolean(false)); //truthey (an object!)
 
trutheyTester(""); //falseytrutheyTester(new String("")); //truthey (an object!)
 
trutheyTester(NaN); //falseytrutheyTester(new Number(NaN)); //truthey (an object!)
需要注意的是,使用 new 运算符返回的总是一个对象。
另外值得一提的是,在使用 new 创建两个值相同的变量时,对两者进行比较的话都是返回 false :
var a = new String('a');
var b = new String('a');
console.log(a == b); //false
console.log(a === b); //false ,这个更不用说。Number 类型也是同样道理。
神奇吧?有些人觉得这无法接受,结果实在是太难预测了,有些人则觉得,这是 JavaScript 特有的特性,加以好好利用的话,则会在一些地方得到灵活的运用。你是怎么想的呢?

好吧下面来看看 == 运算符的规则:

== 运算符

== 运算符相对来说是比较自由的,容忍度比较高。它允许两个不同类型的变量进行比较。其实这里面的奥秘则是,**在进行比较之前,解析器会将两个变量转换成同一种类型(通常是数字),然后再进行比较。**而当两个变量是同一种类型时,那么结果则跟 === 运算符一样。除了 undefined 和 null两个在 == 运算符中是相等的之外,其他大部分都是先转换成数字再进行比较。来看看详细的规则:
(当两个变量均为相同类型值时,参考全等运算符的规则。)
/image/dea3d93e-1f83-4ccb-aba2-1bf8a4159443/a0b1fa4b-2a20-4c8f-a23c-832b6ed8297e_Untitled.png
除了上表所示的几种类型之外,其余的都是 false 。其中 toNumber() 和 toPrimitive() 是解析器内部的两个私有方法。它主要的功能是根据特定的规则转换传入的参数。其具体的规则为:

toNumber() :

/image/dea3d93e-1f83-4ccb-aba2-1bf8a4159443/3064e903-9399-45b9-80e2-ce8ba3308429_Untitled.png

toPrimitive():

/image/dea3d93e-1f83-4ccb-aba2-1bf8a4159443/b6bef002-c64a-4661-a2dc-94a1b7f52a79_Untitled.png
以上则是整个转换的规则。要说难其实不难,因为就上面几条规则而已。要说不难的话,其实也很勉强。一开始接触这方面的知识,难免会有点摸不着头脑。接下来就来演示几个例子,消化消化一下。

[0] == true

[0] == true
// 首先将布尔值转换成数字
[0] == 1
// 接着将 [0] 转换成基本类型值
// 由于 [0].valueOf() 返回一个数组,不是一个基本类型值,则调用
// [0].toString() ,返回 "0"
"0" == 1
// 再将字符串转换成数字
0 == 1 // false,值不同。

[0] == false

[0] == false
// 首先将布尔值转换成数字
[0] == 0
// 接着将 [0] 转换成基本类型值。和上面的一样。于是
0 == 0 // true

'string' == true

'string' == true
// 将布尔值转换成数字
'string' == 1
// 将字符串转换成数字
NaN == 1 // false

'string' == false

'string' == false
// 将布尔值转换成数字,然后将字符串转换成数字。
NaN == 0 // false

Object 对象的 valueOf 方法

var obj = {
	valueOf: function () {
    	return '1';
    }
};
obj == 1
// 调用对象的 `valueOf` 方法,返回字符串 
'1''1' == 1
// 将字符串转换成数字
1 == 1 // true

Object 对象的 toString 方法

var obj = {
	toString: function () {
    	return '1';
    }
};
obj == 1
// 调用对象的 `toString` 方法,返回字符串 '1' ,然后转换成数字
1 == 1 //true

再来演示一下转换对象时,究竟是优先调用 valueOf 还是 toString:
var obj = {
	valueOf: function () {
    	return 0;
    },
    toString: function () {
    	return 1;
    }
};

obj == 0 // true
obj == 1 // false
以上,可以非常直观的看出,转换对象时,会优先调用 valueOf 方法。

现在再来看看全等运算符的情况:

=== 运算符

=== 运算符的情况则简单了许多。
  • 当两个变量不为同一种类型时,总是返回 false 。
  • 当两个变量均为 undefined 或者 null 时返回 true 。
  • 当两个变量均为数字时,当且仅当两者数值相同(但是不为 NaN)时返回 true 。(记住, NaN === NaN 返回 false 。)
  • 当两个变量均为字符串时,当且仅当两者值相同时返回 true 。
  • 当两个变量均为布尔值时,当且仅当两者值相同时返回 true 。
  • 当两个变量均为对象时,当且仅当两者引用的对象一致时返回 true 。
  • 其他情况,一律返回 false 。
那么,这时候我们应该就明白了为什么:
var a = new String('a');
var b = new String('a');

a == b // 两者都是同个类型的变量,转为全等运算。
a === b // 因为使用 new 操作符得到的是一个新的对象,而不是引用某个对象,因此,这里的变量 a 和 b 虽然其 toString() 的值相同,但终究不是引用同一个对象,因此这个运算结果为 false 。
看到这里,你是否有一种豁然开朗的感觉?没错,当初我就是这样的感受!
当遇到一件难以理解的问题时,如果抱着敬而远之的心态的话,那么你有可能会避开了一些“坑”。但是从某些角度看,因为不理解而不去使用,甚至不愿意花时间去深刻挖掘其根本原因,那是一种不科学的学习方式。有时候,迎难而上恰恰是技术提升的关键步骤,而这种提升恰恰能够将你和普通的开发者们区分开来。这就是研究的精华和价值所在。在此,我非常感激让我明白了解这个技术难点的博主!

现在再来看看什么时候应该使用 === ,什么时候使用 ==

综上,我们可以知道使用 == 运算符其实大部分时候都是将变量转换成数字,那么当需要比较的变量一定为数字的时候,我们就可以光明正大的使用 == 进行对比。换句话说,当我们明确知道需要比较的两个变量一定为同类型的值时,我们就可以毫不忌讳的使用 == 运算符进行比较,而不用在乎因为隐式转换带来的问题:
var arr = [1,2,3,4,5]

arr.length == 5 // length 属性返回的始终是一个数字。

arr.length === 5 // 没必要的做法。
if (typeof myVar == 'function') // typeof 操作符返回的始终是一个字符串,而且只有仅有的几种情况。
if (typeof myVar === 'function') // 没必要的做法
嗯,其他的不用说了吧...

Reference