JavaScript 中的類型轉換一直都是讓前端開發者最頭疼的問題。前陣子,推特上有我的專門發了一張圖說 JavaScript 讓人難以想象。前端
除了這個,還有不少經典的、讓 JavaScript 開發者摸不着頭腦的類型轉換,譬以下面這些,你是否知道結果都是多少?面試
1 + {} === ? {} + 1 === ? 1 + [] === ? 1 + '2' === ?
本文將帶領你從 ECMA 規範開始,去深刻理解 JavaScript 中的類型轉換,讓類型轉換再也不成爲前端開發中的攔路虎。函數
JS 中有六種簡單數據類型:undefined
、null
、boolean
、string
、number
、symbol
,以及一種複雜類型:object
。
可是 JavaScript 在聲明時只有一種類型,只有到運行期間纔會肯定當前類型。在運行期間,因爲 JavaScript 沒有對類型作嚴格限制,致使不一樣類型之間能夠進行運算,這樣就須要容許類型之間互相轉換。spa
顯式類型轉換就是手動地將一種值轉換爲另外一種值。通常來講,顯式類型轉換也是嚴格按照上面的表格來進行類型轉換的。prototype
經常使用的顯式類型轉換方法有 Number
、String
、Boolean
、parseInt
、parseFloat
、toString
等等。
這裏須要注意一下 parseInt
,有一道題偶爾會在面試中遇到。翻譯
問:爲何 [1, 2, 3].map(parseInt) 返回 [1,NaN,NaN]?
答:parseInt函數的第二個參數表示要解析的數字的基數。該值介於 2 ~ 36 之間。若是省略該參數或其值爲 0,則數字將以 10 爲基礎來解析。若是它以 「0x」 或 「0X」 開頭,將以 16 爲基數。3d
若是該參數小於 2 或者大於 36,則 parseInt() 將返回 NaN。
通常來講,類型轉換主要是基本類型轉基本類型、複雜類型轉基本類型兩種。
轉換的目標類型主要分爲如下幾種:code
string
number
boolean
我參考了 ECMA-262 的官方文檔來總結一下這幾種類型轉換。ECMA 文檔連接:ECMA-262對象
其餘類型轉換到 number
類型的規則見下方表格:blog
原始值 | 轉換結果 |
---|---|
Undefined | NaN |
Null | 0 |
true | 1 |
false | 0 |
String | 根據語法和轉換規則來轉換 |
Symbol | Throw a TypeError exception |
Object | 先調用toPrimitive,再調用toNumber |
String
轉換爲 Number
類型的規則:
使用+
能夠將其餘類型轉爲 number
類型,咱們用下面的例子來驗證一下。
+undefined // NaN +null // 0 +true // 1 +false // 0 +'111' // 111 +'0x100F' // 4111 +'' // 0 'b' + 'a' + + 'a' + 'a' // 'baNaNa' +Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
原始值 | 轉換結果 |
---|---|
Undefined | false |
Boolean | true or false |
Number | 0和NaN返回false,其餘返回true |
Symbol | true |
Object | true |
咱們也可使用 Boolean
構造函數來手動將其餘類型轉爲 boolean
類型。
Boolean(undefined) // false Boolean(1) // true Boolean(0) // false Boolean(NaN) // false Boolean(Symbol()) // true Boolean({}) // true
原始值 | 轉換結果 | |
---|---|---|
Undefined | 'Undefined' | |
Boolean | 'true' or 'false' | |
Number | 對應的字符串類型 | |
String | String | |
Symbol | Throw a TypeError exception | |
Object | 先調用toPrimitive,再調用toNumber |
轉換到 string
類型能夠用模板字符串來實現。
`${undefined}` // 'undefined' `${true}` // 'true' `${false}` // 'false' `${11}` // '11' `${Symbol()}` // Cannot convert a Symbol value to a string `${{}}`
隱式類型轉換通常是在涉及到運算符的時候纔會出現的狀況,好比咱們將兩個變量相加,或者比較兩個變量是否相等。
隱式類型轉換其實在咱們上面的例子中已經有所體現。對於對象轉原始類型的轉換,也會遵照 ToPrimitive
的規則,下面會進行細說。
在對象轉原始類型的時候,通常會調用內置的 ToPrimitive
方法,而 ToPrimitive
方法則會調用 OrdinaryToPrimitive
方法,咱們能夠看一下 ECMA 的官方文檔。
我來翻譯一下這段話。
ToPrimitive
方法接受兩個參數,一個是輸入的值 input
,一個是指望轉換的類型 PreferredType
。
PreferredType
參數,讓 hint
等於"default"PreferredType
是 hint String
,讓 hint
等於"string"PreferredType
是 hint Number
,讓 hint
等於"number"exoticToPrim
等於 GetMethod(input, @@toPrimitive)
,意思就是獲取參數 input
的 @@toPrimitive
方法exoticToPrim
不是 Undefined
,那麼就讓 result
等於 Call(exoticToPrim, input, « hint »)
,意思就是執行 exoticToPrim(hint)
,若是執行後的結果 result
是原始數據類型,返回 result
,不然就拋出類型錯誤的異常hint
是"default",讓 hint
等於"number"OrdinaryToPrimitive(input, hint)
抽象操做的結果而 OrdinaryToPrimitive
方法也接受兩個參數,一個是輸入的值O,一個也是指望轉換的類型 hint
。
hint
是個字符串而且值爲'string'或者'number'hint
是'string',那麼就將 methodNames
設置爲 toString
、valueOf
hint
是'number',那麼就將 methodNames
設置爲 valueOf
、toString
methodNames
拿到當前循環中的值 name
,將 method
設置爲 O[name]
(即拿到 valueOf
和 toString
兩個方法)method
能夠被調用,那麼就讓 result
等於 method
執行後的結果,若是 result
不是對象就返回 result
,不然就拋出一個類型錯誤的報錯。若是隻用文字來描述,你確定會以爲過於晦澀難懂,因此這裏我就本身用代碼來實現這兩個方法幫助你的理解。
// 獲取類型 const getType = (obj) => { return Object.prototype.toString.call(obj).slice(8,-1); } // 是否爲原始類型 const isPrimitive = (obj) => { const types = ['String','Undefined','Null','Boolean','Number']; return types.indexOf(getType(obj)) !== -1; } const ToPrimitive = (input, preferredType) => { // 若是input是原始類型,那麼不須要轉換,直接返回 if (isPrimitive(input)) { return input; } let hint = '', exoticToPrim = null, methodNames = []; // 當沒有提供可選參數preferredType的時候,hint會默認爲"default"; if (!preferredType) { hint = 'default' } else if (preferredType === 'string') { hint = 'string' } else if (preferredType === 'number') { hint = 'number' } exoticToPrim = input.@@toPrimitive; // 若是有toPrimitive方法 if (exoticToPrim) { // 若是exoticToPrim執行後返回的是原始類型 if (typeof (result = exoticToPrim.call(O, hint)) !== 'object') { return result; // 若是exoticToPrim執行後返回的是object類型 } else { throw new TypeError('TypeError exception') } } // 這裏給了默認hint值爲number,Symbol和Date經過定義@@toPrimitive方法來修改默認值 if (hint === 'default') { hint = 'number' } return OrdinaryToPrimitive(input, hint) } const OrdinaryToPrimitive = (O, hint) => { let methodNames = null, result = null; if (typeof O !== 'object') { return; } // 這裏決定了先調用toString仍是valueOf if (hint === 'string') { methodNames = [input.toString, input.valueOf] } else { methodNames = [input.valueOf, input.toString] } for (let name in methodNames) { if (O[name]) { result = O[name]() if (typeof result !== 'object') { return result } } } throw new TypeError('TypeError exception') }
總結一下,在進行類型轉換的時候,通常是經過 ToPrimitive
方法將引用類型轉爲原始類型。若是引用類型上有 @@toPrimitive
方法,就調用 @@toPrimitive
方法,執行後的返回值爲原始類型就直接返回,若是依然是對象,那麼就拋出報錯。
若是對象上沒有 toPrimitive
方法,那麼就根據轉換的目標類型來判斷先調用 toString
仍是 valueOf
方法,若是執行這兩個方法後獲得了原始類型的值,那麼就返回。不然,將會拋出錯誤。
在 ES6 以後提供了 Symbol.toPrimitive
方法,該方法在類型轉換的時候優先級最高。
const obj = { toString() { return '1111' }, valueOf() { return 222 }, [Symbol.toPrimitive]() { return 666 } } const num = 1 + obj; // 667 const str = '1' + obj; // '1666'
也許上面關於 ToPrimitive
的代碼講解你仍是會以爲晦澀難懂,那我接下來就舉幾個例子來講明對象的類型轉換。
var a = 1, b = '2'; var c = a + b; // '12'
也許你會好奇,爲何不是將後面的 b
轉換爲 number
類型,最後獲得3?
咱們仍是要先看文檔對加號的定義。
首先會分別執行兩個值的 toPrimitive
方法,由於 a
和 b
都是原始類型,因此仍是獲得了1和'2'。
從圖上看到若是轉換後的兩個值的 Type
有一個是 String
類型,那麼就將兩個值通過 toString
轉換後串起來。所以最後獲得了'12',而不是3。
咱們還能夠再看一個例子。
var a = 'hello ', b = {}; var c = a + b; // "hello [object Object]"
這裏還會分別執行兩個值的 toPrimitive
方法,a
仍是獲得了'hello ',而b因爲沒有指定preferredType,因此會默認被轉爲 number
類型,先調用 valueOf
,但 valueOf
仍是返回了一個空對象,不是原始類型,因此再調用 toString
,獲得了 '[object Object]'
,最後將二者鏈接起來就成了 "hello [object Object]"
。
若是咱們想返回 'hello world'
,那該怎麼改呢?只須要修改 b
的 valueOf
方法就行了。
b.valueOf = function() { return 'world' } var c = a + b; // 'hello world'
也許你在面試題中看到過這個例子。
var a = [], b = []; var c = a + b; // ''
這裏爲何 c
最後是''呢?由於 a
和 b
在執行 valueOf
以後,獲得的依然是個 []
,這並不是原始類型,所以會繼續執行 toString
,最後獲得'',兩個''相加又獲得了''。
咱們再看一個指定了 preferredType
的例子。
var a = [1, 2, 3], b = { [a]: 111 }
因爲 a
是做爲了 b
的鍵值,因此 preferredType
爲 string
,這時會調用 a.toString
方法,最後獲得了'1,2,3'
類型轉換一直是學 JS 的時候很難搞明白的一個概念,由於轉換規則比較複雜,常常讓人以爲莫名其妙。可是若是從 ECMA 的規範去理解這些轉換規則的原理,那麼就會很容易知道爲何最後會獲得那些結果。