JS强制类型转换之:比较运算到底发生了什么?
- 24种假值比较引发的思考
- 极端情况下该如何分析
- 比较运算的强制类型转换完全解答
一、24种假值比较引发的思考
摘录《你不知道的JavaScript 中卷》第一部分 类型转换 4.5.3
1 "0" == null; //false 2 "0" == undefined; //false 3 "0" == false; //true 4 "0" == NaN; //false 5 "0" == 0; //true 6 "0" == ""; //false 7 8 false == null; //false 9 false == undefined; //false 10 false == NaN; //false 11 false == 0; //true 12 false == ""; //true 13 false == []; //true 14 false == {}; //false 15 16 "" == null; //false 17 "" == undefined; //false 18 "" == NaN; //false 419 "" == 0; //true 20 "" == []; //true 21 "" == {}; //false 22 23 0 == null; //false 24 0 == undefined; //false 25 0 == NaN; //false 26 0 == []; //true 27 0 == {}; //false
然后还有一种极端情况:
[] == ![]; //true
这里到底发生了什么?
首先这里需要确定的一件事情是,相对等于比较其实是比较逻辑运算的一种,出了绝对等于比较以外的比较都同意遵循强制类型转换机制。那么,强制类型转换的又是什么?做什么样的强制类型转换,这里我们确定的另一间事情是JavaScript的比较到底比较的是什么?
1.1量与本体的对比
通常情况下,比较有两种情况,一种是量的比较,另一种是对象本体的比较。这么解释一下,假设有两个苹果照片,可以比较它们的大小,也可以比较它们是否是同一个苹果,比较大小就是量的比较,而对比它们是否是同一个苹果就是对它们的本体比较。
那在JavaScript的比较逻辑运算中什么是量的比较,什么是本体的比较?
这里我们首相可以想到的是绝对等于比较它就是一种本体比较,在JavaScript中值有两种存储模式,一种是原始值类型直接直接采用栈存储;另一种是引用值采用堆内存,然后变量使用一个栈内存指向堆的数据引用地址。所以绝对比较始终比较的是栈内存的数据,这也是绝对比较比相对比较效率更好的根本原因,也是绝对等于比较不需要进行数据类型转换的根本原因。
{} === {} //false 0 === "0" //false
上面这两个绝对等于比较右说明了两个问题,一种是引用值类型值相等,但作为引用值的本体表现为两个不同的堆内存地址,所以绝对等于对比它们的堆内存地址时是不相等的;原始类型的值都是存在堆内存中,但作为数据本身还有类型的区别,所以Number与String的0在栈内存中表现的数据类型是不同的,所以它们不相等。
量的比较所表达的数学意义也就是大小的比较,而大小比较在数学中不仅仅是数量的比较,还存在这单位的差异。比如一天和一公里这两个数量都是1,但它们的单位指向不同适应范围,用程序的逻辑表达就是这两种比较的任何情况下都是假,回到JavaScript中是不是有一个很熟悉的值,没错,他就是NaN,它不等于任何值,甚至他自己本身都不相等。
let a = NaN; a === a; //false a == a; //false
表现为NaN的情况我们就可以视为无法比较,这种情况任何相对比较返回结果都是假。这一点很关键,后面会出现很多这种情况。
1.2量的比较
在JavaScript各种类型的值中,可以作为量直接比较的当然就是Number类型。然后可以将值间接的转换成Number进行比较的有数字字符串、Date类型的时间戳、字符串的字符在字符集中的编码以及可转换成数字的其他值。
从上面对量的比较分析的来看,JavaScript的相对比较逻辑运算中的强制类型转换就离不开Number、String、Date。这里可能会有点疑惑的是为什么还有Date,Date的比较最终取值是时间搓,也是Number类型。这是因为如果与Date比较的值不是Date类型,这时候要想取时间戳就需要先将这个值强制转换成Date以后再取时间戳。
1.3一个需要排除情况
在前面摘录的24中情况中,不知道你是否注意到null、undefined的八种比较结果都为假,在前面量的比较分析中有这样一段描述“可转换成数字的其他值”,我们都知道null与undefined是可以被转换成Number类型的0值,前面六种情况为假或许我们不会想到这有什么问题,但是当看到它们与Number类型的0比较也为假时,是不是突然疑问重重,然后再来看一行代码:
null == undefined; //true
如果没有这一行代码的出现,你是不是会想到我前面分析的不可比较(NaN)的情况,但上面这行代码彻底打破了这个结论,那这里到底发生了什么?为什么会出现这个结果?
回头再想想比较的两种情况,量的比较和本体的比较,我们知道null所表达的含义是“这个值为空,但它需要分配一个值的内存空间”,而undefined所表达的含义是“这个变量未定义值,且不需要为值分配内存空间”,而我们比较的量和本体都是对值的比较,从值的层面来说它们两个的含义就是一样的,都表示为没有值,所以它们相对等于是相等的。
但是,请注意绝对等于是不相等的,绝对等于只会看栈内存的数据,这两个数据体现在栈内存内还是不一样的。
关于null、undefined的强制类型转换在ES5规范 11.9.3.2-3中,同样在《你不知道的JavaScript 中卷》第一部分 类型转换4.5.2-3中这样一段解释null、undefined比较强制类型转换的显示逻辑代码:
var a = doSomething(); if(a === undefined || a === null){ // .. }
关于undefined、null的这个特例,在值的角度表达就是没有值,所以它们所表达的含义一致,故在相对比较中视为值相等,没有大小没有本体,所以它们除了相互相等就是自身相等,其他任何比较都会返回假值。
二、极端情况下该如何分析
2.1为什么需要强制类型转换?
如果要回答这个问题,就要从JavaScript的语言特性来说,强制类型转换的根本原因就是JavaScript的弱类型特性,变量的类型会随着值的变化而变化,也就是说变量的类型是由值决定的。比如在一个程序中预计的比较是Number值的比较,但是在某个环节中意外的出现了非Number值的赋值,如果不强制类型转换整个程序就无法正常执行下去,所以弱类型特性决定了JavaScript比较逻辑运算必须采用强制类型转换机制。
2.2隐性强制类型转换如何进行
前面我们谈到了量与本体的比较,其实所有的强制类型转换都是围绕这个基本的概念来进行,那什么时候进行量的比较,什么时候进行本体比较呢?
第一条规则:能进行量的比较优先采取量的比较;
比如当比较逻辑运算中出现Number,那强制类型转换就采取强制的Number类型转换;
第二条规则:Boolean、Date类型的值被视为量的比较;
Date作为时间表达对象,时间在通俗意义上就是量的一种,只不过在数据类型中为了可表达性给了它一个特性的类型。Boolean虽然在程序中被用作真假的逻辑意义,但是很多时候采用另一种表达方式,那就是0表示假1表示真,这与语言设计的便捷高效的初衷有关,数值通常只需要占用一个字节,而true、false需要使用多个字节。所以Boolean有时候也会被视作时Number值的另一种应用表达。
第三条规则:字符串类型比较采用字符编码逐个比较模式逐个比较字符序号大小,当能其中一个能比较出结果时就返回该比较结果作为整个字符串的比较结果,否则比较至字符串的最后一个字符;
字符串比较从某种意义上来说,本质上还是量的比较。
第四条规则:对象类型比较实际上只能做本体比较,因为对象属性的复杂性不可能做完全比较,所以这里会先针对对象做toString的强制类型转换,比如当两个值的toString返回值都为"[object Object]"时,然后针对值的栈内存中的引用值堆的物理地址进行比较,如果引用的使用一个值那么物理地址就是相同的,这时候就会相等。
第五条规则:对象的其他子类型可以做到完全比较,其同样适用toString强制类型转换原则,这是因为子类型并不会采用对象的Obeject.prototype.toString(),而是各个子类型重写的toString方法,而这些重写方法实际上发生的String()类型转换,这种情况就可以将整个对象转换成显式的字符串比较。
第六条规则:Object的同类型非同一引用值比较时,适用与比较的一种特殊原则,当两个值不相等时,当强制类型转换不会出现不可比较的情况时,也就是两个非同一引用值的Object比较时大于等于视为非小于,小于等于时视为非大于。
let a = {}; let b = {}; console.log(a == b); //false console.log(a <= b); //true console.log(a >= b); //true
第七条规则:字符串与其他类型的值比较时,如果出现前面两条规则按照前两条的规则比较,其他情况同一采用toString的强制类型转换后的String字符集编码逐一比较原则。
第八条规则:Date类型与Date类型及能转换成Date类型的值进行时间戳比较,其他不能转换的情况都被视为非数比较都为假,但需要注意Number类型的0与new Date(0),这个比较符合Object的同类型比较情况(规则六)。
2.3隐性强制类型转换的优先级与强制类型转换的核心方法toPrimitive
2.3.1优先级:
包含undefined、null的比较不进行任何类型转换,只遵循空值的特殊情况处理,关于这个特殊情况在1.3中已经有详细的分析说明。
Number类型转换大于String类型转换;
Obeject类型比较综合规则四、五、六处理,然后按照规则三、七进行字符串比较处理。
String与非字符串按照前面的优先级分析完,在没有Number类型的转换情况下就同一按照toString处理。
String与Date比较遵循Date的比较规则,如果字符串能转换成Date就比较Date的时间戳,如果不能转换则视为与NaN的比较规则。
2.3.2强制类型转换的核心方法toPrimitive
关于toPrimitive这个方法在ES6之前是引擎的私有方法,ES6在Symber的静态属性中就包含了toPrimitive方法,这个方法的就是JavaScript所有强制类型转换的底层方法,手动模拟代码如下:
//参数根据执行的原始值转换类型分别为string、number、boolean三个字符串Object.prototype[Symbol.toPrimitive] = function(hint){ console.log(hint); switch (hint){ case 'number': return this.valueOf(); case 'boolean': return this.valueOf(); case 'string': return this.toString() === '[object String]' ? this.valueOf() : this.toString(); default: throw new Error(); } }
2.3.3关于Date的类型转换以及转换后的不可比较情况如何处理?
Date类型转换可以接收四种有效值:number、boolean、string、Array。其他类型的值发生Date强制类型转换的结果都为:Invalid Date,这是一种同等于NaN的结果,我把这种情况称为不可比较,因为如果发生这类转换的比较结果都为假。
但是需要注意string、Array不是都能被正常处理的:
字符串只能接收日期和时间的字符串,以及Date.toString()的这类字符串。
new Date(String(new Date())) //Sun Jan 03 2021 17:32:27 GMT+0800 (中国标准时间) new Date("2021 17:32:27") //Fri Jan 01 2021 17:32:27 GMT+0800 (中国标准时间)
数组会取索引为0的原始作为强制类型转换的参数,如果发生数组嵌套的情况也可以逐层转换:
new Date([]) //Invalid Date new Date(["2021 17:32:27"]) //Fri Jan 01 2021 17:32:27 GMT+0800 (中国标准时间) new Date([["2021 17:32:27"]]) //Fri Jan 01 2021 17:32:27 GMT+0800 (中国标准时间)
2.4示例分析
2.4.1:boolean值false先转换成0,然后字符串"0"与Number类型比较也转换成0,所以返回true。
"0" == false //true
2.4.2:注意一下这两种情况发生的强制类型转换规则不是一样的,第一个是String与Array比较,Array类型发生toString转换;第二个是Number与Array比较,Array发生toNumber转换。
"0" == [] //false 0 == [] //true
2.4.3:下面这个案例进一步强化一下String与Array比较的隐式类型转换的规则,Array进行toString转换。
"1,2,3" == [1,2,3] // true
2.4.4:下面这个就是规则六,Object与Object比较,对象先进行toString都得到了"[object Object]",然后进行对象的值栈内存中指向的引用值堆内存的物理地址是否一致。不是一致的情况,它们不相等也不大于不小于对方,但它们的小于等于取非大于的值结果,大于等于取非小于的结果。
{} == {} //false {} <= {} //true !({} > {}) //true 这是上面比较的显式模式 {} >= {} //true !({} < {}) //true 同理
2.4.5:注意Date的转换规则,空字符串的转换结果为“Invalid Date”,这个结果同等与NaN。空数组也空字符串一致。
new Date("") == new Date() //false new Date("") < new Date() //fasle new Date("") > new Date() //false new Date("") <= new Date() //false new Date("") >= new Date() //false
2.4.6:Date比较的特殊情况,0与Date(0)不相等。
0 == new Date(0); //false 0 > new Date(0); //false 0 < new Date(0); //false 0 <= new Date(0); //true 0 >= new Date(0); //true
2.4.7:Date与String、Array的可比较特殊情况(只能做有限的举例,具体可被Date作为构造参数的值参考Date相关文档):
123 < new Date("0"); //true //上面Date对象的时间戳为:946656000000 123 < new Date([0,2,3]); //true //上面Date对象的时间戳为:949507200000
三、比较运算的强制类型转换完全解答
1.undefined与null相互相等且等于自身,它们与其他值比较都不想等。
2.有Boolean的比较发生Number类型转换。
3.NaN不等于任何值,包括不等于它自身。
4.字符串比较实际是逐个比较字符的编码大小,当一个字符比较出结果就返回该比较结果,直至比较到最后一个字符。
5.Object值比较当变量指向同一个引用值时相等;不指向同一个值不相等也不大于不小彼此,但<=、>=返回true。
6.对象子类型比较都是比较String()类型转换后的字符串的字符编码,适应字符串与字符串比较的规则。
7.Date比较分为三种情况,除了这三种情况其他都视为与NaN比较:
Date与Number、Boolean及同类型比较遵循Number类型转换,但需要注意Date(0)与0比较它们不相等不大于不小,但<=、>=返回true;Date(false)与0比较同样适应前面的Date(0)的情况。
Date与String比较,如果String符合Date转换的格式,则先将String转换成Date类型,然后比较时间戳。
Date与Array比较会将Array做String转换,如果字符串符合Date的转换格式则根据上一条规则进行转换比较。