前言
衆所周知, js 是一門弱類型或者說是動態語言。變量沒有類型限制,可以隨時賦予任意值。
雖然變量的數據類型是不確定的,但是各種運算符對數據類型是有要求的。如果運算符發現,運算值的類型與預期不符,就會自動轉換類型。
4 + '1' // '41'
上述代碼中,數值 4
和字符串'1'
相加,結果爲'41'
,在運算過程中,JavaScript 將數值4
自動轉換爲了字符串'4'
,類似的轉換在 JavaScript 中很常見,我們有必要了解下背後的轉換原理。
js數據類型
js 中的數據類型分兩大類:
- 簡單數據類型(也稱爲基本數據類型):Undefined、Null 、Boolean、Number 和 String
- 複雜數據類型:Object
總共 6 種數據類型(ES6 新增了 Symobal 類型,本文不討論),相較於其他的語言,js 中的數據類型好像不足以表示所有數據類型。但是,由於 js 的數據類型具有動態性,因此沒有再定義其他數據類型的必要了。
在類型轉換過程中簡單數據類型又稱爲原始值(原始類型)
強制轉換
JavaScript 內置三個轉型函數,可以使用Number()
、String()
和Boolean()
,手動將各種類型的值,分別轉換成數字、字符串或者布爾值。
Number()
其實把非數值轉換爲數值的函數除了Number()
,parseInt()
和parseFloat()
也經常用到。轉型函數Number()
可以用於任何數據類型,而後面兩個專門用於把字符串轉換爲數值。
原始類型值
- 布爾值,
true
和false
分別被轉換爲1
和0
- 數字值,原樣返回
-
null
值,返回0
-
undefined
,返回NaN
-
字符串
- 如果是空字符串,返回
0
- 如果是字符串中只包含數字(包括前面帶正負號的情況),則將其轉換爲十進制數值(例如
"123"
轉換爲123
),前面的零會被忽略(例如"011"
轉換爲11
) - 如果字符串中是有效的浮點數,如
"1.2"
,返回1.2
(前面的零會被忽略,例如"01.2"
轉換爲1.2
) - 如果是有效的十六進制格式,如
"0xf"
,則將其轉換爲相同大小的十進制整數值 - 其他格式的字符串,則返回 NaN
- 如果是空字符串,返回
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
對象
對象類型的轉換稍微有點複雜:
- 調用對象自身的
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'
parseInt()
因爲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中,也會提示指定第二個參數)
parseFloat()
與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()
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()
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()
函數進行顯式轉換。
ToBoolean
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()
函數。
ToString
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 (){}'
。
ToNumber
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()
之後才繼續執行的,我們有必要總結下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()
方法了。
最後,看幾個有意思的類型轉換例子
- +new Date()
這個返回時間戳的方法相信大家都經常用到,這裏
+
運算符的規則和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
相信不少人在遇到類型轉換尤其是自動轉換的時候,都是靠猜的。現在看了這麼多底層的轉換規則,應該或多或少的對其都有了一定的瞭解了。