淺析js中的類型轉換

前言

衆所周知, 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 分別被轉換爲10
  • 數字值,原樣返回
  • 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
對象

對象類型的轉換稍微有點複雜:

  1. 調用對象自身的valueOf()方法,如果返回原始類型的值,則直接對該值使用Number函數,不再進行後續步驟。
  2. 如果valueOf()方法返回對象,則調用對象自身的toString()方法。如果toString()方法返回原始類型的值,則直接對該值使用Number()函數,不再進行後續步驟。
  3. 如果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()轉換空字符串會返回NaNNumber()對空字符串返回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()方法的執行順序。

  1. 先調用對象自身的toString()方法。如果返回原始類型的值,則對該值使用String()函數,不再進行以下步驟。
  2. 如果toString()方法返回的是對象,再調用對象的valueOf()方法。如果valueOf()方法返回原始類型的值,則對該值使用String()函數,不再進行以下步驟。
  3. 如果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()方法轉換爲字符串(nullundefined沒有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會被轉換爲0undefined會被轉換爲NaN

NaN,即非數值,是一個特殊的數值,用於表示一個將要返回數值而未返回數值的情況。關於這個值,需要注意兩點,第一是任何涉及到與 NaN 的運算操作,都會返回 NaN;第二是 NaN 不等於自身(即 NaN === NaN 爲 false),檢測 NaN 需要使用專用的 isNaN 函數。

相等操作符

==在比較時,如果被比較的兩個值類型不同,那麼系統會自動轉換成相同類型再比較。

==並不是嚴格的比較,只是比較兩者的數值。

在比較不同的數據類型時,有下面幾種情況:

  • 如果有一個操作數爲布爾值,這在比較之前先將其轉換爲數值——false轉換爲0true轉換爲1
  • 如果有一個操作數爲字符串,另一個操作數爲數值,在比較之前先將字符串轉換爲數值。
  • 如果有一個操作數爲對象,另一個操作數不是,則調用對象的valueOf()方法或toString()方法,用得到的基本類型值按照起那麼的規則比較。
  • 如果兩個操作符都爲對象,這比較它們是不是同一個對象,如果都指向同一個對象,則爲true(比較引用)
  • 只要有一個操作數爲NaN,一律返回false
  • nullundefined比較,不會進行轉換,直接返回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()的返回值。

下面是各種類型的返回情況:

  • 數值返回當前數值的字符串形式
  • 字符串直接返回
  • 布爾值返回當前值的字符串形式
  • nullundefined沒有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()方法了。

最後,看幾個有意思的類型轉換例子

  1. +new Date()

    這個返回時間戳的方法相信大家都經常用到,這裏+運算符的規則和Number()轉型函數是相同的,對於對象首先會先調用它的valueOf()方法,Date類型的valueOf()方法返回當前時間的數字值表示(即時間戳),返回結果爲數值,不會繼續下一步調用toSting()方法,直接對當前返回值使用Number()還是返回時間戳

  2. (!+[]+[]+![]).length = 9

    xxx.length,粗略一看有length屬性的就字符串和數組了,所以前面的結果肯定是這兩個其中一種。這裏是類型轉換,不可能出現轉換爲數組的情況,那麼只可能是字符串,下面我們分析一下。

    首先拆開來看,就是!+[][]![]的相加操作

    !+[]會進行如下轉換

    [].toString() // ''
    +'' // 0
    !0 // true

    都轉換完成後,就是true''false的相加操作,結果爲'truefalse',最後結果就是9

  3. [] + {} 和 {} + []

    先說 [] + {} 。一個數組加一個對象。

    []{}valueOf()都返回對象自身,所以都會調用 toString(),最後的結果是字符串串接。[].toString() 返回空字符串,({}).toString() 返回'[object Object]'。最後的結果就是'[object Object]'

    然後說 {} + [] ,看上去應該和上面一樣。但是 {} 除了表示一個對象之外,也可以表示一個空的block

    在 [] + {} 中,[] 被解析爲數組,因此後續的 + 被解析爲加法運算符,而 {} 就解析爲對象。但在 {} + [] 中,開頭的{} 被解析爲空的 block,隨後的 + 被解析爲正號運算符,最後的結果就是0。即實際上成了:

   {} // empty block 
   +[] // 0

相信不少人在遇到類型轉換尤其是自動轉換的時候,都是靠猜的。現在看了這麼多底層的轉換規則,應該或多或少的對其都有了一定的瞭解了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章