總結和剖析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既不大於也不小於任何其餘值。