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] == truefalse ?同样的道理,为啥 if ('string') 是成立的,而 'string' == truefalse?最奇怪的是 'string' == false 居然也是 false? ==

是的,这个 == 运算符看似很奇怪,运算的结果经常会出乎人意料,所以,现在也有很多 JavaScript 大神都在呼吁不要使用 == 运算符进行比较,而应该使用 === 进行比较。因为相对于 == 来说,=== 运算符的运算结果则靠谱多了,或者说,其结果是可以清楚的被预知的。

在我刚开始学 JavaScript 的时候,看到了不少关于这方面的说法。于是乎一开始我也是对这种说法深信不疑,而且对 == 敬而远之。总之,凡是进行比较的运算,我都是会使用 === 而不是使用 == 。甚至会跟身边的小伙伴们说,使用 == 进行比较是一种不合格的写法。

虽然说使用 === 运算符在日常开发中运作得很好,也没有什么值得注意的。但是,对于 == 运算符来说,我是一点都不了解。只知道进行比较之前会进行隐式类型转换。我也曾经疑惑过,我也曾经想弄清楚这里面的奥秘,深入去掌握它,但是终究是太懒,后来也就没有去了解它的欲望了。

直到后来看到了一篇[文章](https://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-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); //falsey
trutheyTester(new Boolean(false)); //truthey (an object!)
 
trutheyTester(""); //falsey
trutheyTester(new String("")); //truthey (an object!)
 
trutheyTester(NaN); //falsey
trutheyTester(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 特有的特性,加以好好利用的话,则会在一些地方得到灵活的运用。你是怎么想的呢?


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

== 运算符

== 运算符相对来说是比较自由的,容忍度比较高。它允许两个不同类型的变量进行比较。其实这里面的奥秘则是,**在进行比较之前,解析器会将两个变量转换成同一种类型(通常是数字),然后再进行比较。**而当两个变量是同一种类型时,那么结果则跟 === 运算符一样。除了 undefinednull 两个在 == 运算符中是相等的之外,其他大部分都是先转换成数字再进行比较。来看看详细的规则:

(当两个变量均为相同类型值时,参考全等运算符的规则。)

变量X类型 变量Y类型 结果
null undefined
true
undefined null
true
Number String
X == toNumber(Y)
String Number
toNumber(X) == Y
Boolean 任何一种
toNumber(X) == Y
任何一种 Boolean
X == toNumber(Y)
String 或者 Number Object
x == toPrimitive(Y)
Object String 或者 Number
toPrimitive(X) == Y

除了上表所示的几种类型之外,其余的都是 false 。其中 toNumber()toPrimitive() 是解析器内部的两个私有方法。它主要的功能是根据特定的规则转换传入的参数。其具体的规则为:

toNumber() :

参数类型 结果
undefined NaN
null +0
Boolean 当参数为 true 时结果为 1,当参数为 false 时结果为+0
Number 返回参数本身。(没有转换)
String 当字符串为数字字符串时返回该数字值,当不为数字字符串时返回 NaN: "123456" -> 123456 "string" -> NaN
Object

按顺序执行这两个步骤:

1. 执行 ToPrimitive(input argument, hint Number),返回 primValue

2. 返回 ToNumber(primValue) 的结果。

toPrimitive():

参数类型 结果
Object 当调用这个对象的 valueOf 方法时如果返回一个基本类型值的话,则返回这个值。不然的话调用这个对象的 toString 方法,如果返回的是一个基本类型值的话,则返回这个值。如果不符合这两种情况则抛出错误。
其他 结果返回参数本身。(没有转换)

以上则是整个转换的规则。要说难其实不难,因为就上面几条规则而已。要说不难的话,其实也很勉强。一开始接触这方面的知识,难免会有点摸不着头脑。接下来就来演示几个例子,消化消化一下。


[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

Comments is loading...

Comments is loading...