总结和剖析JavaScript中的强制类型转换,主要参考《你不知道的JavaScript(中卷)》第四章。javascript
文章内容主要分为五个部分,第一部分讲述向基本类型数据转换的4种抽象操做,第二部分讲述显式强制类型转换,第三部分讲述隐式强制类型转换,第四部分讲述相等关系,其中须要掌握“抽象相等比较算法”,再结合第一部分的4种抽象操做,基本上能清楚JS中的类型转换过程,第五部分讲述比较关系。java
抽象操做ToString负责处理非字符串到字符串的强制类型转换。算法
基本类型值的字符串化规则为:null转换为"null",undefined转换为"undefined",true转换为""true"。数字的字符串化遵循通用规则,那些极小和极大的数字使用指数形式:数组
var a = 1.07*1000*1000*1000*1000*1000*1000*1000
a.toString() //"1.07e21"
复制代码
对普通对象来讲,除非自行定义,不然toString()返回内部属性[[Class]]的值,如"[object Object]"。安全
数组的默认toString()方法通过了从新定义,将全部单元字符串化之后再用","链接起来:bash
var a = [1,2,3]
a.toString() //"1,2,3"
复制代码
抽象操做ToNumber将非数字值转换为数字值。app
其中true转换为1,false转换为0,undefined转换为NaN,null转换为0。函数
ToNumber对字符串的处理基本遵循数字常量的相关规则(字符串中含有非数字类型字符返回NaN)。工具
对象(包括数组)会首先被转换为相应的基本类型值,若是返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。ui
抽象方法ToPrimitive将对象值转换为相应的基本类型值。该方法会首先检查该值是否有valueOf()方法,若是有而且返回基本类型值,就使用该值进行强制类型转换;若是没有就使用toString()的返回值(若是存在)来进行强制类型转换;若是valueOf()和toString()均不返回基本类型值,会产生TypeError错误。
从ES5开始,使用Object.create(null)建立的对象原型属性为null,而且没有valueOf()和toString()方法,所以没法进行强制类型转换。
抽象操做ToBoolean将非布尔值转换为布尔值。
假值的布尔强制类型转换结果为false。
如下这些是假值:
从逻辑上说,假值列表之外的都应该是真值,可是JavaScript规范对此并无明肯定义,只是给出了一些实例,例如规定全部的对象都是真值。
例如:
var a = new Boolean(false)
var b = new Boolean(0)
var c = new Boolean("")
var d = Boolean(a && b && c) //true
复制代码
a,b,c都是封装了假值的对象,可是d为true,说明a、b、c都为true。所以假值对象并不是封装了假值的对象。
假值对象看起来和普通对象并没有二致,但将它们强制类型转换为布尔值时结果为false。最多见的例子是document.all,它是一个类数组对象,包含了页面上全部元素,它之前曾是一个真正意义上的对象,布尔强制类型转换结果为true,不过如今它是一个假值对象。
真值就是假值列表以外的值。真值列表能够无限长,没法一一列举,因此只能用假值列表做为参考。
字符串和数字之间的转换是经过String()和Number()这两个内建函数来实现的,请注意它们前面没有new关键字,并不建立封装对象。
String()遵循前面讲过的ToString规则,将值转换为字符串基本类型。Number()遵循前面讲过的ToNumber规则,将值转换为数字基本类型。
除了String()和Number()之外,还有其余方法能够实现字符串和数字之间的显示转换:
var a = 42
var b = a.toSting() //"42"
var c = "3.14"
var d = +c //3.14
复制代码
a.toString()是显式的,不过其中涉及隐式转换。由于toString()对42这样的基本类型值并不适用,因此JavaScript引擎会自动为42建立一个封装对象,而后对该对象调用toString()。
上例中+c是+运算符的一元形式(即只有一个操做数)。+运算符显式地将c转换为数字,而非数字加法运算(也不是字符串拼接)。
一元运算符 - 和 + 同样,而且还会反转数字的符号位。因为 -- 会被看成递减运算符来处理,因此咱们不能使用 -- 来撤销反转,而应该像 - -"3.14"这样,在中间加一个空格。
尽可能不要把一元运算符 + (还有 - )和其余运算符放在一块儿使用。此外d = +c也容易和d += c搞混,二者天壤之别。
一元运算符 + 的另外一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳。
var time = new Date()
+time
复制代码
~首先将值强制类型转换为32位数字,而后执行字位操做“非”(对每个字位进行反转)。
字位反转是个很晦涩的主题,JavaScript开发人员通常不多须要关心到字位级别。
对~还能够有另一种诠释:返回2的补码!
因此 ~x 大体等同于 -(x+1)。
~42 //-(42+1) ==> -43
复制代码
在-(x+1)中惟一可以获得0(或者严格说是-0)的x值是-1,而在JavaScript中有些函数用-1来表明执行失败,用大于等于0的值来表明函数执行成功。
好比,indexOf()方法在字符串中搜索指定的字符串,若是找到就返回子字符串的位置,不然返回-1。
var a = "Hello World"
if(a.indexOf("lo") != -1){
// 找到匹配
}
if(a.indexOf("ol") == -1){
// 没有找到匹配
}
复制代码
~和indexOf()一块儿能够将结果强制类型转换为真/假值,若是indexOf()返回-1,~将其转换为假值0,其余状况一概转换为真值。
var a = "Hello World"
~a.indexOf("lo") // -4 ==>真值
if(~a.indexOf("lo")){
// 找到匹配
}
~a.indexOf("ol") // 0 ==>假值
if(!~a.indexOf("ol")){
// 没有找到匹配
}
复制代码
~~x能将值截除为一个32为整数,~~中的的第一个~执行ToInt32并反转字位,而后第二个~再进行一次字位反转,即将全部字位反转回原值,最后获得的仍然是ToInt32的结果。
首先~~只适用于32位数字,更重要的是它对负数的处理与Math.floor()不一样。
Math.floor(-49.6) // -50
~~-49.6 //-49
复制代码
解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。可是解析和转换二者之间仍是有明显的差异。好比:
var a = "42"
var b = "42px"
Number(a) //42
parseInt(a) //42
Number(b) //NaN
parseInt(b) //42
复制代码
解析容许字符串中含有非数字字符,解析按从左到右的顺序,若是遇到非数字字符就中止。而转换不容许出现非数字字符,不然会失败返回NaN。
解析字符串中的浮点数可使用parseFloat()函数。从ES5开始parseInt()默认转换为十进制数,除非指定第二个参数做为基数。
不要忘了parseInt()针对的是字符串,向parseInt()传递数字和其余类型的参数是没有用的。非字符串会首先被强制类型转换为字符串,应该避免向parseInt()传递非字符串参数。
parseInt(1/0,19) //18
复制代码
parseInt(1/0,19) 最后的结果是18,而非报错,由于parseInt(1/0,19)其实是parseInt("Infinity",19)。基数19,它的有效数字字符范围是0-9和a-i(区分大小写),以19为基数时,第一个字符"I"值为18,而第二个字符"n"不是一个有效的数字字符,解析到此为止,和"42px"中"p"同样。
和前面讲过的+类型,一元运算符!显式地将值强制类型转换为布尔值。可是它同时还将真值反转为假值。因此显式强制类型转换为布尔值最经常使用地方法是!!。
在if()这样的布尔值上下文中,建议使用Boolean()和!!来进行显式转换以便让代码更清晰易读。
ES5规范中定义:若是某个操做数是字符串或者可以经过如下步骤转换为字符串的话,+将进行拼接操做。若是其中一个操做数是对象(包括数组),则首先对其调用ToPrimitive抽象操做,该抽象操做再调用[[DefaultValue]],以数字做为上下文。
简单来讲就是,若是+的其中一个操做数是字符串(或者经过以上步骤能够获得字符串),那么就执行字符串拼接,不然执行数字加法。
var a = [1,2]
var b = [3,4]
a + b //"1,23,4"
复制代码
由于数组的valueOf()操做没法获得简单基本类型值,因而调用toString(),所以两个数组变成了"1,2"和"3,4",+将它们拼接后返回。
a + ""(隐式)和前面的String(a)(显式)之间有一个细微的差异须要注意。根据ToPrimitive抽象操做规则,a + ""会对a调用valueOf()方法,而后经过ToString抽象操做将返回值转换为字符串,而String(a)则是直接调用toString()方法。
若是其中有且仅有一个参数为true,则onlyOne()返回true。
function onlyOne() {
var sum = 0
for (var i=0;i<arguments.length;i++){
if(arguments[i]){
sum += arguments[i]
}
}
return sum == 1
}
var a = true
var b = false
onlyOne(b,a,b,b,b,b) // true
复制代码
不管使用隐式转换仍是显式转换,咱们均可以经过修改onlyTwo()或者onlyFive()来处理更加复杂的状况,只须要将最后的条件判断从改成2或5。这比加入一大堆&&和||表达式要简洁得多。
相对布尔值,数字和字符串操做中的隐式强制类型转换还算比较明显。下面的状况会发生布尔值隐式强制类型转换。
其实不太赞同将它们称为“逻辑运算符”,由于这不太准确。称它们为“选择器运算符”或者“操做数选择器运算符”更恰当一些。
ES5规范中说到:&&和||运算符的返回值并不必定是布尔类型,而是两个操做数其中一个的值。
对于||来讲,若是条件判断结果为true就返回第一个操做数的值,若是为false就返回第二个操做数的值。
对于&&来讲,若是条件判断结果为true就返回第二个操做数的值,若是为false就返回第一个操做数的值。
** 这里的条件判断结果指的是左边的操做数。
下面是一个十分常见的||的用法,也许你已经用过但却并未彻底理解:
function foo(a,b) {
a = a||"hello"
b = b||"world"
console.log(a + '' + b)
}
foo() // "hello world"
复制代码
再来看看&&,有一种用法开发人员不常见,然而JavaScript代码压缩工具经常使用。就是若是第一个操做数为真值,则&&运算符选择第二个操做数做为返回值,这也叫作“守护运算符”,即前面的表达式为后面的表达式把关。
function foo() {
console.log(a)
}
var a = 42
a && foo()
复制代码
ES6中引入了符合类型,它的强制类型转换有一个坑。ES6容许从符合到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误,例如:
var s1 = Symbol("cool")
String(s1) // "Symbol(cool)"
var s2 = Symbol("not cool")
s2 + '' // TypeError
复制代码
符合不可以被强制类型转换为数字(显式和隐式都会产生错误),但能够被强制类型转换为布尔值(显式和隐式都是true)。
常见的误区是“==检查值是否相等,===检查值和类型是否相等”,正确的解释是:“==容许在相等比较中进行强制类型转换,而===不容许”。事实上,==和===都会检查操做数的类型,区别在于操做数类型不一样时它们的处理方式不一样。
ES5规范11.9.3节的“抽象相等比较算法”定义了==运算符的行为。该算法简单而又全面,涵盖了全部可能出现的类型组合,以及它们进行强制类型转换的方式。
比较运算x==y, 其中x和 y是值,产生true或者false。这样的比较按以下方式进行:
1. 若Type(x)与Type(y)相同, 则
a. 若Type(x)为Undefined, 返回true。
b. 若Type(x)为Null, 返回true。
c. 若Type(x)为Number, 则
i. 若x为NaN, 返回false。
ii. 若y为NaN, 返回false。
iii. 若x与y为相等数值, 返回true。
iv. 若x 为 +0 且 y为−0, 返回true。
v. 若x 为 −0 且 y为+0, 返回true。
vi. 返回false。
d. 若Type(x)为String, 则当x和y为彻底相同的字符序列(长度相等且相同字符在相同位置)时返回true。 不然, 返回false。
e. 若Type(x)为Boolean, 当x和y为同为true或者同为false时返回true。 不然, 返回false。
f. 当x和y为引用同一对象时返回true。不然,返回false。
2. 若x为null且y为undefined, 返回true。
3. 若x为undefined且y为null, 返回true。
4. 若Type(x) 为 Number 且 Type(y)为String, 返回comparison x == ToNumber(y)的结果。
5. 若Type(x) 为 String 且 Type(y)为Number,返回比较ToNumber(x) == y的结果。
6. 若Type(x)为Boolean, 返回比较ToNumber(x) == y的结果。
7. 若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果。
8. 若Type(x)为String或Number,且Type(y)为Object,返回比较x == ToPrimitive(y)的结果。
9. 若Type(x)为Object且Type(y)为String或Number, 返回比较ToPrimitive(x) == y的结果。
10. 返回false。
复制代码
var a = 42
var b = '42'
a == b //true
复制代码
a==b是宽松相等,即若是两个值的类型不一样,则对其中之一或二者都进行强制类型转换。具体怎么转换?这就须要匹配前文的“抽象相等比较算法”,寻找适应的转换规则。
根据第4条规则返回x == ToNumber(y)的结果。
==最容易出错的一个地方是true和false与其余类型之间的相等比较。
var a = '42'
var b = true
a == b //false
复制代码
结果是false,这让人很容易掉坑里。若是严格按照“抽象相等比较算法”,这个结果也就是意料之中的。
根据第7条规则,若Type(y)为Boolean, 返回比较x == ToNumber(y)的结果,即返回'42' == 1,结果为false。
很奇怪吧?因此不管什么状况下都不要使用== true和== false。
在==中null和undefined相等,这也就是说在==中null和undefined是一回事,能够相互进行隐式强制类型转换。
** 掌握“抽象相等比较算法”,读者能够自行推倒为何[]==![]返回true。
if(a == 2 && a == 3){
//...
}
复制代码
你也许以为这不可能,由于a不会同时等于2和3。但若是让a.valueOf()每次调用都产生反作用,好比第一次返回2,第二次返回3,就会出现这样的状况。
var i = 2
Number.prototype.valueOf = function() {
return i++
}
var a = new Number(42)
if(a == 2 && a == 3){
console.log('Yeah, it happened!')
}
复制代码
还有一个坑经常被提到:
0 == '\n' //true
复制代码
""、"\n"(或者" "等其余空格组合)等空字符串被ToNumber强制类型转换为0。
再来看看那些“短”的地方:
"0" == false // true
false == 0 // true
false == "" // true
false == [] // true
"" == 0 // true
"" == [] // true
0 == [] // true
复制代码
其中有4种状况涉及== false,以前咱们说过应该避免,因此还剩下后面3种。
这些特殊状况会致使各类问题,使用中要多加当心。咱们要对==两边的值认真推敲,如下两个原则可让咱们有效地避免出错。
隐式强制转换在部分状况下确实很危险,为了安全起见就要使用===
以 x 和 y 为值进行小于比较(x < y 的比较),会产生的结果可为="" true,false或 undefined(这说明 x、y 中最少有一个操做数是 NaN)。除了 x 和 y,这个算法另外须要一个名为 LeftFirst 的布尔值标记做为参数。这个标记用于解析顺序的控制,由于操做数 x 和 y 在执行的时候会有潜在可见的反作用。LeftFirst 标志是必须的,由于 ECMAScript 规定了表达式是从左到右顺序执行的。LeftFirst 的默认值是 true,这代表在相关的表达式中,参数 x 出如今参数 y 以前。若是 LeftFirst 值是 false,状况会相反,操做数的执行必须是先 y 后 x。这样的一个小于比较的执行步骤以下:
1. 若是 LeftFirst 标志是 true,那么
a. 让 px 为调用 ToPrimitive(x, hint Number) 的结果。
b. 让 py 为调用 ToPrimitive(y, hint Number) 的结果。
2. 不然解释执行的顺序须要反转,从而保证从左到右的执行顺序
a. 让 py 为调用 ToPrimitive(y, hint Number) 的结果。
b. 让 px 为调用 ToPrimitive(x, hint Number) 的结果。
3. 若是 Type(px) 和 Type(py) 获得的结果不都是 String 类型,那么
a. 让 nx 为调用 ToNumber(px) 的结果。由于 px 和 py 都已是基本数据类型(primitive values 也做原始值),其执行顺序并不重要。
b. 让 ny 为调用 ToNumber(py) 的结果。
c. 若是 nx 是 NaN,返回 undefined
d. 若是 ny 是 NaN,返回 undefined
e. 若是 nx 和 ny 的数字值相同,返回 false
f. 若是 nx 是 +0 且 ny 是 -0,返回 flase
g. 若是 nx 是 -0 且 ny 是 +0,返回 false
h. 若是 nx 是 +∞,返回 fasle
i. 若是 ny 是 +∞,返回 true
j. 若是 ny 是 -∞,返回 flase
k. 若是 nx 是 -∞,返回 true
l. 若是 nx 数学上的值小于 ny 数学上的值(注意这些数学值都不能是无限的且不能都为 0),返回 ture。不然返回 false。
4. 不然,px 和 py 都是 Strings 类型
a. 若是 py 是 px 的一个前缀,返回 false。(当字符串 q 的值能够是字符串 p 和一个其余的字符串 r 拼接而成时,字符串 p 就是 q 的前缀。注意:任何字符串都是本身的前缀,由于 r 多是空字符串。)
b. 若是 px 是 py 的前缀,返回 true。
c. 让 k 成为最小的非负整数,能使得在 px 字符串中位置 k 的字符与字符串 py 字符串中位置 k 的字符不相同。(这里必须有一个 k,使得互相都不是对方的前缀)
d. 让 m 成为字符串 px 中位置 k 的字符的编码单元值。
e. 让 n 成为字符串 py 中位置 k 的字符的编码单元值。
f.若是 n<m,返回 true。不然,返回 false。
复制代码
下面的例子就有些奇怪了:
var a = {b:42}
var b = {b:43}
a < b // false
a == b // false
a > b // false
a <= b // true
a >= b // true
复制代码
若是a < b和a == b结果为false,为何a <= b和a >= b的结果会是true呢?
由于根据规范a <= b被处理为b < a,而后将结果反转。由于b < a的结果为false,因此a <= b的结果为true。
这可能与咱们设想的截然不同,即<=应该是“小于或者等于”,实际上,JavaScript中<=是“不大于”的意思,即a <= b被处理为 !(b < a)。
** 规范设定NaN既不大于也不小于任何其余值。