衆所周知, js 是一門弱類型或者說是動態語言。變量沒有類型限制,能夠隨時賦予任意值。javascript
雖然變量的數據類型是不肯定的,可是各類運算符對數據類型是有要求的。若是運算符發現,運算值的類型與預期不符,就會自動轉換類型。java
4 + '1' // '41'
上述代碼中,數值 4
和字符串'1'
相加,結果爲'41'
,在運算過程當中,JavaScript 將數值4
自動轉換爲了字符串'4'
,相似的轉換在 JavaScript 中很常見,咱們有必要了解下背後的轉換原理。express
js 中的數據類型分兩大類:數組
總共 6 種數據類型(ES6 新增了 Symobal 類型,本文不討論),相較於其餘的語言,js 中的數據類型好像不足以表示全部數據類型。可是,因爲 js 的數據類型具備動態性,所以沒有再定義其餘數據類型的必要了。瀏覽器
在類型轉換過程當中簡單數據類型又稱爲原始值(原始類型)函數
JavaScript 內置三個轉型函數,可使用Number()
、String()
和Boolean()
,手動將各類類型的值,分別轉換成數字、字符串或者布爾值。工具
其實把非數值轉換爲數值的函數除了Number()
,parseInt()
和parseFloat()
也常常用到。轉型函數Number()
能夠用於任何數據類型,然後面兩個專門用於把字符串轉換爲數值。測試
true
和false
分別被轉換爲1
和0
null
值,返回0
undefined
,返回NaN
字符串eslint
0
"123"
轉換爲123
),前面的零會被忽略(例如"011"
轉換爲11
)"1.2"
,返回1.2
(前面的零會被忽略,例如"01.2"
轉換爲1.2
)"0xf"
,則將其轉換爲相同大小的十進制整數值Number(true) // 1 Number(false) // 0 Number(10) // 10 Number(null) // 0 Number(undefined) // NaN Number('') // 0 Number('10') // 10 Number('010') // 10 Number('1.2') // 1.2 Number('01.2') // 1.2 Number('0xf') // 15 Number('hello') // NaN
對象類型的轉換稍微有點複雜:code
valueOf()
方法,若是返回原始類型的值,則直接對該值使用Number
函數,再也不進行後續步驟。valueOf()
方法返回對象,則調用對象自身的toString()
方法。若是toString()
方法返回原始類型的值,則直接對該值使用Number()
函數,再也不進行後續步驟。toString()
方法返回的是對象,則報錯var obj = {a: 1} Number(obj) // NaN // 等同於下面步驟 if (typeof obj.valueOf() === 'object') { Number(obj.toString()) } else { Number(obj.valueOf()) }
上面代碼中,首先調用valueOf()
,結果返回對象自己;繼續調用toString()
,返回字符串[object Object]
,對此字符串調用Number()
,返回NaN
。
大多數狀況下,對象的valueOf()
方法返回對象自身,因此通常會調用toString()
方法,而toString()
方法通常返回對象的類型字符串(例如[object Object]
),因此Number()
的參數是對象時,通常返回NaN
。
若是toString()
方法返回的不是原始類型的值,就會報錯,咱們能夠重寫對象的toString()
方法:
var obj = { valueOf: function() { return {} }, toString: function() { return {} } } Number(obj) // TypeError: Cannot convert object to primitive value
數組類型的狀況有點不同,對於空數組,轉換爲0
;對於只有一個成員(且該成員爲可以被Number()
轉換的值即轉換結果不爲NaN
)的數組,轉換爲該成員的值;其餘狀況下的數組,轉換爲NaN
。
Number([]) // 0 Number([10]) // 10 Number([null]) // 0 Number([[10]]) // 10
數組類型和通常對象的轉換結果不同,主要是由於數組的toString()
方法內部重寫了,直接調用不會返回[object Array]
,而是返回每一個成員(會先轉換成字符串)拼接成的字符串,中間以逗號分隔。
[10].toString() // '10' [null].toString() // '' [1, 10].toString() // '1,10'
由於Number()
函數在轉換字符串時比較複雜並且不太適用,所以在處理字符串轉換爲整數的時候更經常使用的是parseInt()
函數。parseInt()
函數在轉換字符串時,更多的是看起其是否符合數值模式。它會忽略字符串前面的空格,直至找到第一個非空格字符。
parseInt()
就會返回NaN
;所以,用parseInt()
轉換空字符串會返回NaN
(Number()
對空字符串返回0
)。parseInt()
會繼續解析第二個字符,直到解析完全部後續字符或者遇到了一個非數字字符。例如,'123abc'
會被轉換爲1234
,由於'abc'
會被徹底忽略;'22.5'
會被轉換爲22
,由於小數點不是有效的數字字符。parseInt()
也可以識別出各類整數格式(十進制、八進制、十六進制)。若是字符串以'0x'
開頭且後面是數字字符,就會當成一個十六進制整數;若是字符串以0
開頭且後面是數字字符,則會將其當作八進制整數。parseInt('123abc') // 123 parseInt('') // NaN parseInt('a12') // NaN parseInt('0xa') // 10(十六進制數) parseInt('12.5') // 12 parseInt('070') // 56(八進制數)
上述代碼中parseInt('070')
在瀏覽器中測試會返回10
,前面的零會被忽略掉。其實parseInt()
能夠傳入第二個參數,轉換的基數(想要以什麼進制來轉換)
parseInt('0xaf') // 175 parseInt('af') // NaN // af 原本是有效的十六進制數值,若是指定第二個參數爲16,使用十六進制來解析,那麼能夠省略前面的 0x parseInt('af', 16) // 175
若是不指定基數,parseInt()
會自行肯定如何解析輸入的字符串,爲了不沒必要要的錯誤,應該老是傳入第二個參數,明確指定基數。
通常狀況下,解析的都是十進制數值,應該始終將 10 做爲第二個參數(在語法檢查工具 ESLint中,也會提示指定第二個參數)
與parseInt()
函數相似,parseFloat()
也是從第一個字符開始解析每一個字符,一直解析到最後一個字符。若是碰見一個無效的浮點數字字符,就會中止解析,輸出結果。字符串中的第一個小數點是有效的,而第二個小數點就是無效的了,所以它後面的字符都會被忽略。例如,'12.34.5'
會被轉換爲12.34
。
parseFloat()
會忽略最前面的零,且它只能解析十進制值(沒有第二個參數)。
parseFloat('123abc') // 123 parseFloat('0xa') // 0 parseFloat('12.3') // 12.3 parseFloat('12.3.4') // 12.3 parseFloat('012.3') // 12.3
Boolean()
函數能夠將任意類型的值轉爲布爾值。至於返回的這個值是true
仍是false
,取決於要轉換值的數據類型及其實際值。下表給出了各類數據類型及其對應的轉換規則。
數據類型 | 轉換爲true的值 | 轉換爲false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | ""(空字符串) |
Number | 任何非零數字值(包括無窮大) | 0(包括-0和+0)和NaN |
Object | 任何對象 | null |
Undefined | - | undefined |
須要注意的是,全部空對象的轉換結果都是true
,並且布爾對象表示的false
值(new Boolean(false)
)的轉換結果也是true
。
Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true
String()
函數能夠將任意類型的值轉化成字符串,轉換規則以下。
true
轉換爲字符串'true'
,false
轉換爲字符串'false'
undefined
,轉爲字符串'undefined'
null
,轉爲字符串'null'
String(123) // '123' String('abc') // 'abc' String(true) // 'true' String(undefined) // 'undefined' String(null) // 'null'
String()
函數的參數若是是對象,返回一個類型字符串;若是是數組,返回該數組的字符串形式。
String()
函數背後的轉換規則,與Number()
函數基本相同,區別是互換了valueOf()
方法和toString()
方法的執行順序。
toString()
方法。若是返回原始類型的值,則對該值使用String()
函數,再也不進行如下步驟。toString()
方法返回的是對象,再調用對象的valueOf()
方法。若是valueOf()
方法返回原始類型的值,則對該值使用String()
函數,再也不進行如下步驟。valueOf()
方法返回的是對象,就報錯。String({name: 1}) // '[object Object]' // 等同於 String({name: 1}.toString()) String('[object Object]') // '[object Object]'
上面代碼先調用對象的toString()
方法,會返回字符串'[object Object]'
,而後對該值使用String()
函數,不會再調用valueOf()
方法,輸出'[object Object]'
。
若是toString()方
法和valueOf()
方法,返回的都是對象,就會報錯,能夠重寫對象的這兩個方法來驗證:
var obj = { valueOf: function() { return {} }, toString: function() { return {} } } String(obj) // TypeError: Cannot convert object to primitive value
類型轉換中一個難點就是自動轉換(也稱隱式轉換),因爲在一些操做符下其類型會自動轉換,這使得 js 尤爲靈活,但同時又難以理解。
JavaScript 會自動轉換數據類型的狀況主要有三種:
不一樣類型的數據互相運算
1 == '1' // true 1 + 'a' // '1a'
條件控制語句的條件表達式爲非布爾值類型
if ('a') { console.log('success') } // 'success'
對非數值類型的值使用一元運算符(+
和-
)
+{a: 1} // NaN -[1] // -1
自動轉換的規則是這樣的:預期什麼類型的值,就調用該類型的轉換函數。好比,某個位置預期爲字符串,就調用String()
函數進行轉換。若是該位置便可以是字符串,也多是數值,那麼默認轉爲數值。
因爲自動轉換具備不肯定性,並且不易排錯,建議在預期爲布爾值、數值、字符串的地方,所有使用Boolean()
、Number()
和String()
函數進行顯式轉換。
JavaScript 遇到預期爲布爾值的地方(好比if
語句的條件部分),就會將非布爾值自動轉換爲布爾值,自動調用Boolean()
函數,其轉換規則在上面已經說過。
這些轉換規則對於條件類語句很是重要,在運行過程當中,會自動執行相應的Boolean
轉換。
var str = 'abc' if (str) { console.log('success') } // 'success' str && 'def' // 'def'
運行上面代碼,就會輸出字符串success
,由於字符串abc
被自動轉換成了對應的Boolean
值(true)。因爲存在這種自動執行的Boolean
轉換,所以在條件類語句中知道是什麼類型的變量很是重要。
常見的自動轉換布爾值還有下面兩種:
// 三目運算符 expression ? true : false // 雙重取反 !!expression
上面兩種寫法,內部其實仍是調用的Boolean()
函數。
JavaScript 遇到預期爲字符串的地方,就會將非字符串值自動轉爲字符串。字符串的自動轉換,主要在字符串的加法運算,當一個值爲字符串,另外一個值爲非字符串時,非字符串會自動轉換。
自動轉換通常會調用非字符串的toString()
方法轉換爲字符串(null
和undefined
沒有toString()
方法,調用String()
方法轉換爲'null'
和'undefined'
),轉換完成獲得相應的字符串值,而後就是執行加法運算,即字符串的拼接操做了。
'1' + 2 // '12' '1' + true // '1true' '1' + false // '1false' '1' + {} // '1[object Object]' // 數組的 toString 方法通過重寫,返回每一個成員以逗號分隔拼接成的字符串 // 下面一行等同於, '1' + '', 結果爲 '1' '1' + [] // '1' '1' + function (){} // '1function (){}' '1' + undefined // '1undefined' '1' + null // '1null'
上面代碼須要注意的是字符串與函數的加法運算,會先調用函數的toString()
方法,它始終返回當前函數的代碼(看起來就像源代碼通常)。
(function (){}).toString() // 'function (){}' (function foo(a){ return a + b }).toString() // 'function foo(a){ return a + b }'
因此轉換以後就是'1'
和'function (){}'
的拼接操做,結果爲'1function (){}'
。
JavaScript 遇到預期爲數值的地方,就會將非數值自動轉換爲數值,系統內部會自動調用Number()
函數。
除了加法運算符(+
)有可能(可能會是字符串拼接的狀況)把自動轉換,其餘運算符都會把非數值自動轉成數值。Number()
函數的轉換規則前面已經說過,下面看看幾個例子:
'3' - '2' // 1 '3' * '2' // 6 true - 1 // 0 false - 1 // -1 '1' - 1 // 0 '3' * [] // 0 false / '3' // 0 'a' - 1 // NaN null + 1 // 1 undefined + 1 // NaN
上面代碼中須要注意的是,null
會被轉換爲0
,undefined
會被轉換爲NaN
NaN,即非數值,是一個特殊的數值,用於表示一個將要返回數值而未返回數值的狀況。關於這個值,須要注意兩點,第一是任何涉及到與 NaN 的運算操做,都會返回 NaN;第二是 NaN 不等於自身(即 NaN === NaN 爲 false),檢測 NaN 須要使用專用的 isNaN 函數。
==
在比較時,若是被比較的兩個值類型不一樣,那麼系統會自動轉換成相同類型再比較。
==
並非嚴格的比較,只是比較二者的數值。
在比較不一樣的數據類型時,有下面幾種狀況:
false
轉換爲0
,true
轉換爲1
。valueOf()
方法或toString()
方法,用獲得的基本類型值按照起那麼的規則比較。true
(比較引用)NaN
,一概返回false
null
和undefined
比較,不會進行轉換,直接返回true
下面看看幾個例子:
false == 0 // true true == 1 // true true == 3 // false '5' == 5 // true var o = {name: 'foo'} o == 1 // false o == '[object Object]' // true,前面的 o 對象轉換爲基礎類型值 '[object Object]' undefined == 0 // false null == 0 // false null == undefined // true 'NaN' == NaN // false 1 == NaN // false NaN == NaN // false
在開發時,在遇到不肯定是字符串仍是數值時,可能會使用==
來比較,這樣就能夠不用手動強制轉換了。但其實這是偷懶的表現,仍是規規矩矩的先強制轉換後再比較,這樣代碼看上去邏輯會更清晰(若是配置了語法檢測工具eslint
,它的建議就是一直使用===
而不使用==
,避免自動轉換)
前面提到了這麼多轉換規則,有不少都是調用的toString()
以後才繼續執行的,咱們有必要總結下toString()
的返回值。
下面是各類類型的返回狀況:
null
和undefined
沒有toString()
方法,通常使用String()
,分別返回'null'
和'undefined'
'[object Object]'
,'[object Math]'
)function foo(){}
返回'function foo(){}'
(各瀏覽器返回可能有差別)join()
方法),例如:[1, 2, 3]
返回'1,2,3'
new Date('2018-10-01 12:12:12').toString()
返回'Mon Oct 01 2018 12:12:12 GMT+0800 (中國標準時間)'
。但其實在類型轉換中Date
類型通常是調用valueOf()
,由於它的valueOf()
方法返回當前時間的數字值表示(即時間戳),返回結果已是基礎類型值了,不會再調用toString()
方法了。最後,看幾個有意思的類型轉換例子
這個返回時間戳的方法相信你們都常常用到,這裏+
運算符的規則和Number()
轉型函數是相同的,對於對象首先會先調用它的valueOf()
方法,Date
類型的valueOf()
方法返回當前時間的數字值表示(即時間戳),返回結果爲數值,不會繼續下一步調用toSting()
方法,直接對當前返回值使用Number()
仍是返回時間戳
(!+[]+[]+![]).length = 9
xxx.length,粗略一看有length
屬性的就字符串和數組了,因此前面的結果確定是這兩個其中一種。這裏是類型轉換,不可能出現轉換爲數組的狀況,那麼只多是字符串,下面咱們分析一下。
首先拆開來看,就是!+[]
、[]
、![]
的相加操做
!+[]
會進行以下轉換
[].toString() // '' +'' // 0 !0 // true
都轉換完成後,就是true
、''
、false
的相加操做,結果爲'truefalse'
,最後結果就是9
先說 [] + {} 。一個數組加一個對象。
[]
和 {}
的 valueOf()
都返回對象自身,因此都會調用 toString()
,最後的結果是字符串串接。[].toString()
返回空字符串,({}).toString()
返回'[object Object]'
。最後的結果就是'[object Object]'
。
而後說 {} + [] ,看上去應該和上面同樣。可是 {}
除了表示一個對象以外,也能夠表示一個空的block
。
在 [] + {} 中,[]
被解析爲數組,所以後續的 +
被解析爲加法運算符,而 {}
就解析爲對象。但在 {} + []
中,開頭的{}
被解析爲空的 block
,隨後的 +
被解析爲正號運算符,最後的結果就是0
。即實際上成了:
{} // empty block +[] // 0
相信很多人在遇到類型轉換尤爲是自動轉換的時候,都是靠猜的。如今看了這麼多底層的轉換規則,應該或多或少的對其都有了必定的瞭解了。