在開始本文以前咱們一塊兒來看看JavaScript神奇的隱式轉換:javascript
0 + '1' === '01' // true
true + true === 2 // true
false === 0 // false
false + false === 0 // true
{} + [] === 0 // true
[] + {} === 0 // false
複製代碼
更多千奇百怪的例子相信你們在逛各類技術社區和平常工做的時候也見到很多,這裏就不作更多介紹,若是你能充分理解上述隱式轉化的過程,那基本能夠點下右上角的x。前端
本文旨在梳理JS中的數據類型及其對應的轉化關係,從本文你能夠了解到:java
要講清楚隱式轉換,不可避免要嘮嘮類型,JS中按大類分有兩大類型,分別是基本類型和Object,說到這可能有小夥伴會質疑,明明還有Array、Date...本質上其JS中其餘的高級類型都Object的子類型,本文後續統一將Array、Date等類型統稱爲Object類型。面試
包括ES6新增的symbol,JS中一共有6種基礎類型:Symbol、null、undefined、number、string、boolean;加上Object,JS種一共有七種內置類型。bash
通常狀況下咱們可使用typeof操做符去判斷內置類型:antd
typeof Symbol() === 'symbol' // true
typeof undefined === 'undefined' // true
typeof true === 'boolean' // true
typeof 42 === 'number' // true
typeof '42' === 'string' // true
typeof { bar: 42 } === 'object' // true
// 但還有一個例外
typeof null === 'object' // true
// 這個bug是因爲typeof的底層實現,和null的底層表示有關係這裏就不展開了
複製代碼
既然typeof沒法區分Array和Date,那咱們如何區分Object的子類型呢,在JS實現這些子類型時候爲它們增長了一個內部屬性[[Class],咱們能夠經過Object.prototype.toString()進行查看。函數
Object.prototype.toString.call(/i/g) // "[object RegExp]"
Object.prototype.toString.call(Date.now()) // "[object Date]"
複製代碼
須要注意的是Object.prototype.toString應該只用來區分已經斷定了Object的類型:post
var num = 42
var numObj = new Number(42)
typeof num // number
typeof numObj // object
Object.prototype.toString.call(num) // "[object Number]"
Object.prototype.toString.call(numObj) // "[object Number]"
// 能夠看到Object.prototype.toString並不能很好的區分基礎類型和Object
// 這是由於num toString的過程當中會被包裝成封裝對象,結束後解封爲基礎類型
複製代碼
全部的隱式轉換都是基於強制類型轉換的,因此咱們要搞清楚JS中強制轉換是如何運做的。學習
在ECMAScript第五版規範中定義了抽象操做ToString,規範定義了其餘類型強制轉化爲string類型的過程,JS中強制轉化爲string類型的方法通常是:String(...)ui
咱們看下下面的例子:
String(4) // "4"
String(false) // "false"
String(true) // "true"
String(null) // "null"
String(undefined) // "undefined"
String(Symbol('s')) // "Symbol(s)"
// 基礎類型強制轉string類型在規範中明確說明了,也比較符合咱們的直覺
// 可是Object類型就有些許差異
String({ a: 2 }) // "[object Object]"
String([1, 2]) // "1,2"
String(/reg/g) // "/reg/g"
// 能夠看到Object的子類型之間toString並不一致
// 實際上在對Object類型進行toString轉換的時候,
// 會調用原型鏈上的toString方法,並做爲結果返回
var arr = [1, 2];
arr.toString() // "1,2"
String(arr) // "1,2"
// 重寫toString
arr.toString = function() { return this.join('/') };
String(arr) // "1/2"
// 可見Object類型在強制轉換爲string類型的時候,
// 實際是調用了該類型原型上的toString方法,
// 而Object的各個子類型基本都重寫了toString方法
// 因此在進行toString操做的時候表現有差別
複製代碼
JS規範一樣還定義了其餘類型強制轉換爲number類型的抽象過程,咱們觀察下面的例子:
Number("4") // 4
Number("4a") // NaN
Number("") // 0
Number(false) // 0
Number(true) // 1
Number(null) // 0
Number(undefined) // NaN
Number(Symbol('s')) // TypeError...
複製代碼
對於基本類型的強制轉換都是在規範中寫死,須要注意的是Symbol類型在強制轉number的過程當中會報TypeError,算是一個坑。咱們重點關注一下Object類型轉number的過程,對象在轉number以前,會先轉換爲基礎類型,再轉換爲number類型,這個過程稱爲ToPrimitive。
ToPrimitive過程先回檢查對象是否存在valueOf方法,若是存在而且valueOf返回基本類型的值,則使用該值進行強制類型轉換,若是沒有,則使用toString方法返回的值進行強制類型轉換。
var arr = [1, 2]
Number(arr) // NaN
// 由於arr.toString()等於"1,2",強制轉換後爲NaN
arr.toString = function() { return '43' }
Number(arr) // 43
arr.valueOf = function() { return '42' }
Number(arr) // 42
var obj1 = {}
Number(obj1) // NaN
var obj2 = {
valueOf: function () {
return '99'
}
}
Number(obj2) // 99
複製代碼
剛剛咱們討論了不少,JS中強制轉換的規則,那其實和隱式類型有什麼關係呢?
JS在進行隱式轉換的過程當中,其實式遵照強式轉換的規則的,因此咱們探討隱式類型轉換本質是探討[] + {} 是怎麼樣經過一系列的類型轉換變成"[object Object]"。
在隱式轉換中最使人迷惑的應該就是+操做符和==操做符致使的隱式轉換,由於對於其餘類型的操做符,類型四則運算的-、*、÷和位運算符&、^、|在設計目標就是對數字進行操做。
咱們觀察下列代碼:
10 / '2' // 5 對字符串2進行了ToNumber操做
'10' / '5' // 2 對操做符兩邊進行了ToNumber操做
var obj = {
valueOf: function() {
return '10'
}
}
100 / obj
// 10 對obj進行了ToNumber操做,感到迷惑的同窗能夠翻上去看看抽象操做ToNumber的執行過程
// 對於位運算也是一致的
0b011 | '0b111' // 7
複製代碼
說完簡單的,咱們來看看真正噁心人的。
對於JavaScript來講,+號除了傳統意義的四則運算,還有鏈接字符串的功能。
1 + 2 // 3
'hello' + ' ' + 'world' // hello world
複製代碼
有歧義就會使人迷惑,那麼到底何時適用字符串鏈接,何時是加法呢?
觀察下列代碼:
1 + '1' // "11"
1 + true // 2
1 + {} // "1[object Object]"
'1' + {} // "1[object Object]"
1 + [] // "1"
var obj = {
valueOf: function() { return 1 }
}
1 + obj // 2
var obj2 = {
toString: function() { return 3 }
}
1 + obj2 // 4
var obj3 = {
toString: function() { return '4' }
}
1 + obj3 // "14"
複製代碼
看完上面的例子,應該是有點暈的,總結下來就是,若是其中一個操做數是字符串;或者其中一個操做數是對象,且能夠經過ToPrimitive操做轉換爲字符串,則執行字符串鏈接操做;其餘狀況執行加法操做。
// 經過僞碼描述過程大概就是
x + y
=> if (type x === string || type y === string ) return join(x, y)
=> if (type x === object && type ToPrimitive(x) === string) return join(x, y)
=> if (type y === object && type ToPrimitive(y) === string) return join(x, y)
=> else return add(x, y)
複製代碼
對於執行加法操做的狀況,若是操做數有一邊不是number,則執行ToNumber操做,將操做數轉換爲數字類型。
咱們一塊兒來分析兩個例子:
// 例子1
[1, 2] + {} // "1,2[object Object]"
/** * [1, 2]和{}均不是字符串,可是[1, 2]和{}都可以經過ToPrimitive操做 * 可是[1, 2]和{}都可以經過ToPrimitive操做轉換爲字符串 * 因此這裏執行字符串鏈接操做,根據ToPrimitive的規則 * [1, 2].valueOf()的值不是基礎類型,因此咱們使用[1, 2].toString()的值 * 這時候就變成了 "1,2" + {} * 顯然{}也能夠經過ToPrimitive操做轉換爲"[object Object]" * 因此最後的結果是"1,2[object Object]" **/
// 例子2
var obj = {
valueOf: function() { return 12 }
}
true + obj // 13
/** * true和變量obj均不是字符串,且obj不能經過ToPrimitive轉換爲字符串 * 因此這裏執行加法操做 * 對true執行ToNumber操做獲得1 * 對obj執行ToPrimitive操做獲得12 * 最後1 + 12 輸出12 **/
複製代碼
經過上面的例子相信你們已經對+號兩邊的隱式轉換有必定了解了,可是一些同窗確定會說那爲啥{} + [] === 0呢,這個明顯不符合上述過程,這的確是一個坑,這個坑在於編譯器並不會想我沒預想的那般將{}解析成對象,而是解析成代碼塊。
{} + []
/** * 對於編譯器而言,代碼塊不會返回任何的值 * 接着+[]就變成了一個強制轉number的過程 * []經過oPrimitive變成'',最後''經過ToNumber操做轉換成0 **/
{}; +[];
複製代碼
說完這些,相信你們對本文開始的幾個例子輸出的結果不會迷惑了,除了+號兩邊使人迷惑,最使人迷惑的莫過於==,以至於大部分前端團隊都會經過eslint禁止使用==操做,下面咱們一塊兒來揭開==之謎。
==操做符被稱爲抽象相等,也是夠抽象的,通常來講咱們會建議禁止在業務代碼中使用抽象相等。
但有時候用起來卻很方便,好比antd中的下拉框選項中即便咱們拉取的數據是number類型,在onChange回調中value的值倒是字符串,這時候使用抽象相等就挺舒服的。
實際開始討論抽象相等的轉換規則以前,咱們先看下特例:
NaN == NaN // false,這算是個坑吧,沒啥聊的
null == undefined // true,屬於ecma規範
複製代碼
說完特例,咱們看看其餘狀況下==的表現是如何的:
[1] == 1 // true
false == '0' // true
false == '' // true
'' == '0' // false
true == 1 // true
false == 0 // true
true == [] // false
[] == {} // false
var obj = {
valueOf: function() { return 1 }
}
obj == 1 // true
// 絕望
[] == ![] // true
複製代碼
看着好像和以前的類型轉換有些一致,但跟可能是懵逼,咱們一塊兒來看看ecma規範中是如何描述抽象相等的比較過程的:
說完規則,咱們來根據規則分析幾個例子:
true == '1' // true
/** * 布爾類型和其餘類型比較適用規則2,true經過ToNumber操做轉換爲1 * 這時候1 == '1',這時候適用規則1,將'1'經過ToNumber操做轉換爲1 * 1 == 1 因此輸出爲true **/
var obj = {
valueOf: function() { return '1' }
}
true == obj // true
/** * 首先適用規則2,將true轉換爲1,此時1 == obj * 此時適用規則3,將obj轉換爲'1',此時1 == '1' * 此時適用規則1,將'1'轉換爲1,此時1 == 1,因此輸出true **/
// 咱們分析下世紀難題 [] == ![]的心路歷程
[] == ![] // true
/** * 通常直覺這明細是false,但咱們仔細看一下 * ![]先對[]進行強制boolean轉換,因此實際上應該是[] == false * 這樣就又回到咱們剛剛的規則上了,適用規則2因此[] == 0 * 接着適用規則3,因此 '' == 0 * 最後ToNumber('') == 0 **/
複製代碼
到這裏,基本上JS上比較常見的隱式類型覆蓋和坑都覆蓋到了,其實能夠看到隱式類型並非無跡可尋,除了少數特例,基本上都是依據一些規則進行轉換的,咱們只須要記住轉換規則,就可以收放自如了。
隱式類型轉換是新手學習前端的時候常常碰到的坑,咱們經常推薦使用===,放棄對==的理解,可是即便咱們不使用,在學習社區上的一些代碼的時候,不可避免的會遇到有人使用的狀況,因此即便本身不使用==,看別人代碼也不可避免要看,因此知道原理仍是有必要的。
最後的最後留一些小小的練習題:
var obj = {
valueOf: function() { return 42 },
toString: function() { return '42' },
}
var arr = [1, 2]
1 + obj
arr + obj
0 == []
"" == []
obj == '42'
複製代碼
真的最後了,感謝各位同窗的閱讀,若是有錯誤但願可以在評論區指出,萬分感謝。
歡迎閱讀個人其餘文章: