ToPrimitive == === > <
参考《你不知道的JavaScript(中卷)》第四章
理解 ToPrimitive 操作 就能理解了JS 中的有点迷的 ==
操作了
// 以下是有点奇怪的 ==
"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
0 == []; // true
ToPrimitive 操作
抽象操作 ToPrimitive(参见 ES5 规范 9.1 节)会首先(通过内部操作 DefaultValue,参见 ES5 规范 8.12.8 节)检查该值是否有 valueOf() 方法。
如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
什么是 ToNumber
true
转换为 1, false
转换为 0。 undefined
转换为 NaN
, null
转换为 0。ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法。处理失败时返回 NaN
(处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的十六进制数并不按十六进制处理(而是按十进制)
== 和 ===
==
允许在相等比较中进行强制类型转换,而 ===
不允许。
实际上 ==
和 ===
都会检查操作数的类型。区别在于操作数类型不同时它们的处理方式不同。
特别情况
NaN == NaN // false
+0 == -0 // true
ES5 规范 11.9.3.1 的最后定义了对象(包括函数和数组)的宽松相等(==):两个对象指向同一个值时即视为相等,不发生强制类型转换。
在比较两个对象的时候, ==
和 ===
的工作原理是一样的
== 比较不同类型时发生了什么
显然会进行类型转换
基本数据类型都转为数字
对象类型进行 ToPrimitive 操作
转换的优先级是 布尔 > 字符串 > 对象
最终他们都会转为 数字 类型(因为基本数据类型都会转为数字类型,对象的转换优先级最低,轮到对象进行转换的时候,另外一个需要转换的操作数早就转为数字了,如果 ToPrimitive 操作返回的结果非数字,那么要进行 == 操作的两个操作数的类型依然不同, ToPrimitive 操作返回的结果还需要转为数字)
举个例子,布尔值和字符串比较:
false == "abc" // false
// 转换的优先级是 布尔 > 字符串,所以 false 先转为数字得到 0
// 现在相当于比较 0 == "abc",操作数的类型还是不同,继续类型转换
// "abc" 转为数字,Number("abc") 得到 NaN
// 0 == NaN 结果 false
布尔值和对象比较:
false == [] // true
// 转换的优先级是 布尔 > 对象,所以 false 先转为数字得到 0
// 现在相当于比较 0 == [],操作数的类型还是不同,继续类型转换
// [] 是对象,进行 ToPrimitive 操作,得到空字符,
// 现在相当于比较 0 == "",操作数的类型还是不同,继续类型转换
// 空字符是基本数据类型,转为数字,Number("") 得到 0
// 0 == 0 结果 true
那么 null 和 undefined 呢?这里引出另外一个问题:包装类型(看最后)
他们不会进行转换
null 只会 == undefined 或者自身,undefined 同样
null == null // true
null == undefined // true
undefined == undefined // true
对 [] 和 {} 呢?
以下代码不是说明 [] == {}
发生了什么,因为两个对象进行宽松或者严格相等时,不进行类型转换,两个对象指向同一个值时即视为相等
// 对象和基本数据类型进行宽松比较时,对象发生了什么?
// 当对象是 []
[].valueOf() // 还是一个对象
[].toString() // '' 一个空字符串
Number("") // 0
// 当对象是 [2,3]
[2,3].valueOf() // 还是一个对象
[2,3].toString() // "2,3"
Number("2,3") // NaN
// 当对象是 [null]
[null].valueOf() // 还是一个对象
[null].toString() // ""
Number([null]) // 0
/*
也许你认为 [null].toString() 返回的不是 ""
但是如果不这样处理的话又能怎样呢?
有人也许会觉得既然 String(null) 返回 "null"
所以 String([null]) 也应该返回 "null"。
确实有道理,实际上这是 String([..]) 规则的问题。
又或者根本就不应该将数组转换为字符串?
但这样一来又会导致很多其他问题
*/
// 当对象是 {}
({}).valueOf() // 还是一个对象
({}).toString() // "[object Object]"
Number( "[object Object]") // NaN
>, <, <=
这属于抽象关系比较:
比较双方首先调用 ToPrimitive,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较。
实际上 JavaScript 中 <= 是 “不大于” 的意思(a <= b 被处理为 b < a,然后将结果反转。)即 a <= b ,处理为 !(b < a)。
相等比较有严格相等,关系比较却没有“严格关系比较”(strict relational comparison)。也就是说如果要避免 a < b 中发生隐式强制类型转换,我们只能确保 a 和 b 为相同的类型,除此之外别无他法。
包装类型
基本数据类型:number、string、boolean 都有包装类型,这是为了让这些基本数据类型可以方便地调用一些常用的方法,比如 toString,valueOf 等等
但是 null 和 undefined 没有对应的包装类型,所以 null 和 undefined 不能够被封装(boxed)
Object(null) 和 Object() 均返回一个常规对象。
“拆封”,即“打开”封装对象(如 new String("abc")),返回其中的基本数据类型值("abc")。
以上说的和 ==
有什么关系?
因为 ==
中的 ToPromitive 强制类型转换也会发生拆封,这大概就是很多人错误地认为 == 不进行类型判断的原因(我猜的)
var a = "abc";
var b = Object( a ); // 和new String( a )一样
a === b; // false
a == b; // true
Object(null) 和 Object() 均返回一个常规对象,没法拆封。
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