imundefined, 微信公众号前端 Q 原创, 申诉证明
== 操作符(Equality,相等操作符)
相等操作符会做类型转换。
我们先来看看什么是类型转换(type coercion)
当操作符两边的操作数是不同类型时,其中一个操作数将转换为另一个操作数同类型的“等效”值。比如:
12 - '3' // 9, 将 string 类型转换成 number 类型,相当于 12 - Number('3')
12 - 'hello' // NaN, 因为 Number('hello') 为 NaN,12 - NaN 为 NaN
'3' - 12 // -9, 同样转成 number 类型
12 - true // 11, Number(true) 为 1
false - 12 // -12, Number(false) 为 0
12 + '3' // 123, 将 12 转换成 string 类型,再连接 '3'
看上去很简单对不对?减法将非 number 类型的转换成 number 类型的,加法将 number 类型转换成 string 类型的,真的这样吗?看下面:
12 + true // 13,这里将布尔类型 true 转换成数值类型 1,相当于 12 + Number(true)
从上面可以看出,“加法将 number 类型 转换成 string 类型”这个总结并不正确,事实上,类型转换并不单单看操作符,也就是并不只是看你做加法还是做减法,还要看你的类型,我们从上面的规律中做下总结:
- number - string 将 string 转换成 number
- string - number 将 string 转换成 number
- number - boolean 将 boolean 转成 number
- boolean - number 将 boolean 转成 number
- number + string 将 number 转成 string
- number + boolean 将 boolean 转成 number
- ...
有点眼花,也不是很有规律。有时候类型转换还可能会调用 toString 方法,加上操作符有很多,像 +、-、*、/、%、、= 等等,再加上 5 种基本数据类型 Number、String、Undefined、Null、Boolean,还有引用类型 Object,这样组合起来,类型转换的规律就很难掌握了。
比如下面这个:
[1] - 3 // -2, 相当于 Number([1].toString()) - 3
[1, 2, 3] - 3 // NaN, 这里 toString 之后 会得到 '1,2,3',转成 number 就是 NaN
[[]] - 3 // -3, [[]].toString 是 "","" 转成 number 类型是 0
5 * '1' // 5, Number('1') 为 1
5 * 'true' // NaN, Number('true') 为 NaN
5 * true // 5, Number(true) 为 1
是不是觉得规则已经比较难记忆了?
回到我们的 == 操作符
相等操作符在比较时,如果左右两边的类型不同,也会将左边或者右边的操作数转换成与对方同类型的等效值。比如:
12 == "12" // true, string 类型转换成 number 类型
1 == true // true, boolean 类型转换成 number 类型
true == '1' // true, 两者都转换成 number 再比较? 还是 '1' 转换成 boolean 类型?
true == '2' // false?? Boolean('2') 可是等于 true 哦,结合上面一条语句,可以得出应该是两者都转换成 number 再比较
再来看看 == 操作符的传递性
0 == '' // true
0 == '0' // true
'' == '0' // false
可以看出,== 操作符不满足传递性
再来看几个例子:
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
'
' == 0 // true
是不是觉得还能理解,但可能记不住了?所以,如果你不了解所有的规则,最好不要使用相等操作符,而是使用恒等操作符,下面这张图比较有说服力,来自 dorey.github.io,绿色表示相等操作符运算后结果为 true:
规则这么多,掌握不好容易出错。
=== 操作符(Identity,恒等操作符)
很多人认为恒等操作符代表值相等,并且有相同的类型,但这是不对的,来看下面的例子:
var a = [1, 2, 3],
b = [1, 2, 3],
c = {},
d = {};
console.log(a === b); // false
console.log(c === d); // false
可见,即使是相同的类型,而且值相等,也不一定恒等。
恒等
恒等操作符有三种情况:
- 两个操作数都是引用类型,他们都引用同一个 object,即地址相同,则恒等成立。
- 两个操作数都是基本数据类型 (Boolean、Number、String、Undefined、Null),如果值相同,则恒等成立。
- 一个操作数是引用类型,另一个是基本数据类型,恒等不成立。
上面的例子不等是因为地址不同。我们再来看看比较特殊的 String:
String 的基本数据类型和基本包装类型
我们知道,基本数据类型不是引用类型,应该是没有方法的,比如 'hello world',但你确实可以用 'hello world'.split("") 分割字符串,这是因为调用 split 这个方法的时候,后台就会创建一个基本包装类型的对象 (new String('hello world')),我们实质上是从这个基本包装类型里获取的方法,这样的基本包装类型还有 Boolean 和 Number。
要明确,'hello world' 只是一个基本数据类型,不管它是拼接而成还是直接以字面量的形式形成,比如:
var a = 'hello' + ' world',
b = 'hello world';
console.log(a === b); // true
因为 a、b都是基本数据类型,所以直接比较值,是相等的,那么恒等成立。
但如果是这样:
var a = 'hello world',
b = new String('hello world');
console.log(a === b); // false, 因为 a 是基本数据类型,b 是引用类型,恒等不成立。
来看一下恒等操作符的比较情况:
所以 Douglas Crockford 建议我们永远不使用 == 操作符,而是使用 === 操作符
另外,在引用类型的比较上 == 和 === 的表现还是一致的,因为不论是否有类型转换,值都是地址,即比较的都是地址,地址相同则相同,如有不对,还望指出:
var a = [1, 2, 3],
b = [1, 2, 3],
c = {},
d = {},
e = a;
console.log(a === b); // false
console.log(c === d); // false
console.log(a == b); // false
console.log(c == d); // false
console.log(a == e); // true
console.log(a === e); // true
console.log([0] == (new String("0"))); // false
console.log([0] === (new String("0"))); // false
参考
Which equals operator (== vs ===) should be used in JavaScript comparisons?
JavaScript-Equality-Table
《JavaScript 高级程序设计》