1. 程式人生 > >JS做型別檢測到底有幾種方法?看完本文就知道了!

JS做型別檢測到底有幾種方法?看完本文就知道了!

JS有很多資料型別,對於不同資料型別的識別和相互轉換也是面試中的一個常考點,本文主要講的就是型別轉換和型別檢測。 ## 資料型別 JS中的資料型別主要分為兩大類:原始型別(值型別)和引用型別。常見的資料型別如下圖所示: ![image-20200506103537269](https://user-gold-cdn.xitu.io/2020/5/6/171ea49f4035263a?w=402&h=308&f=png&s=23212) 原始資料型別存在棧中,引用型別在棧中存的是一個引用地址,這個地址指向的是堆中的一個數據物件。需要注意的是`null`在這裡我們算在原始型別裡面,但是你用`typeof`的時候會發現他是`object`,原因是就算他是一個物件,那他應該在棧中存一個引用地址,但是他是一個空物件,所以這個地址為空,也就是不對應堆中的任意一個數據,他在堆中沒有資料,只存在於棧中,所以這裡算為了原始型別。引用型別其實主要就是`Object`,`Array`和`Function`這些其實也都是`Object`派生出來的。[關於這兩種型別在記憶體中的更詳細的知識可以看這篇文章。](https://juejin.im/post/5e2155cee51d4552455a8878) ![image-20200506104330145](https://user-gold-cdn.xitu.io/2020/5/6/171ea4a37f695852?w=140&h=37&f=png&s=3248) 下面我們來看看這兩種型別的區別: ### 原始型別 1. 原始型別的值無法更改,要更改只能重新賦值。像下面這樣嘗試去修改是不行的,但是整個重新賦值可以。 ![image-20200506104958681](https://user-gold-cdn.xitu.io/2020/5/6/171ea4a83fb1875a?w=256&h=67&f=png&s=5626) ![image-20200506105044457](https://user-gold-cdn.xitu.io/2020/5/6/171ea4aaa2689f75?w=258&h=67&f=png&s=6195) 2. 原始型別的比較就是比較值,值相等,他們就相等 ### 引用型別 1. 引用型別的值是可以修改的,注意這個時候我們雖然修改了`a`裡面的屬性,但是`a`在棧上的引用地址並沒有變化,變化的是堆中的資料。 ![image-20200506105513907](https://user-gold-cdn.xitu.io/2020/5/6/171ea4afef4a408e?w=132&h=100&f=png&s=4609) 2. 引用型別的比較是比較他們的索引地址,而不是他們的值。比如下面兩個物件,看著是一樣的,但是他們的引用地址不一樣,其實是不等的: ![image-20200506110135018](https://user-gold-cdn.xitu.io/2020/5/6/171ea4b22ff449f5?w=142&h=119&f=png&s=4797) 要想讓他們相等,得直接將`b`賦值為`a`,這樣他們的引用地址一樣,就是相等的。 ![image-20200506110256501](https://user-gold-cdn.xitu.io/2020/5/6/171ea4b503ff3924?w=148&h=90&f=png&s=4555) ## 型別轉換 JS中當不同型別的資料進行計算的時候會進行型別轉換,比如下面的例子: ![image-20200506110621714](https://user-gold-cdn.xitu.io/2020/5/6/171ea4b78e4029fe?w=258&h=109&f=png&s=10955) 上面的例子中,我們用了加減來操作幾個非數字的型別,這時候JS會進行隱式的型別轉換,然後再進行加減運算。除了JS本身的隱式轉換外,有時候我們還會主動進行型別轉換,這就算是顯示型別轉換了。 ### 隱式型別轉換 #### 轉為字串 經常出現在`+`運算中,並且其中有一個運算元不是數值型別 ```javascript let s = 4 + 'px' + 5; console.log(s); // 4px5 s = 123e-2 + 'a'; console.log(s); // 1.23a ``` #### 轉為數值 經常出現在數學運算中,表示連線字串的`+`運算除外 ```javascript let s = 'abc'; console.log(+s, -s); // NaN, NaN s = ' 123 '; console.log(+s, -s); // 123 -123 s = new Date(); console.log(+s, -s); // 1588675647421 -1588675647421 (這個操作相當於取毫秒數) ``` #### 轉為bool的場景 經常出現在if或者邏輯運算中 ```javascript let s = 'abc'; if(s) { console.log(s); // abc } console.log(!!s); // true ``` 下面的值在進行bool轉換時會轉換為`false`,除此以外都是`true`: 1. 0 2. NaN 3. ''(空字串) 4. null 5. undefined #### ==運算子 當我們使用`==`進行比較時,如果兩邊的型別不同,JS會進行型別轉換,然後再比較,`===`則不會進行型別轉換,如果`===`兩邊的資料型別不同,直接返回`false`。 ![image-20200506112606774](https://user-gold-cdn.xitu.io/2020/5/6/171ea4c1c5a91170?w=380&h=166&f=png&s=19353) 上面只是列舉了其中幾種情況,更多的情況可以參考下面這種表,這個表來自於MDN。這個表的內容比較多,有些是規範直接定義的,比如`null == undefined`,也沒有太多邏輯可言。我們不確定時可以來查下這個表,但是實際開發中其實是不建議使用`==`的,因為如果你把這個轉換關係記錯了的話可能就會引入比較難排查的bug,一般推薦直接使用`===`。 ![image-20200506111718423](https://user-gold-cdn.xitu.io/2020/5/6/171ea4c66e83d521?w=992&h=380&f=png&s=197731) #### 轉換規則 下面這幾張表是一些轉換規則,來自於《JS權威指南》: ![image-20200505185955549](https://user-gold-cdn.xitu.io/2020/5/6/171ea4c9d0689ba1?w=1166&h=501&f=png&s=145951) ![image-20200505190049837](https://user-gold-cdn.xitu.io/2020/5/6/171ea4cc3ace9205?w=1169&h=381&f=png&s=118894) ![image-20200505190124871](https://user-gold-cdn.xitu.io/2020/5/6/171ea4ce966831fd?w=1166&h=376&f=png&s=127366) ### 顯式型別轉換 顯式型別轉換是我們自己寫程式碼明確轉換的型別,可以使程式碼看起來更清晰,是實際開發時推薦的做法。 ![image-20200506113002845](https://user-gold-cdn.xitu.io/2020/5/6/171ea4d237fa48e1?w=284&h=164&f=png&s=13704) #### 轉字串 顯式轉換為字串可以使用`toString`方法,它的執行結果通常和`String()`方法一致。Number型別的`toString`方法還支援引數,可以指定需要轉換的進位制。下面的圖是一些原始型別的`toString()`,`null`和`undefined`沒有`toString`方法,呼叫會報錯: ![image-20200506113217062](https://user-gold-cdn.xitu.io/2020/5/6/171ea4d5efb36c93?w=511&h=191&f=png&s=28794) Number型別的`toString`方法支援進位制: ![image-20200506113346662](https://user-gold-cdn.xitu.io/2020/5/6/171ea4d7f3061572?w=263&h=53&f=png&s=5728) #### 轉數值 轉為數值就很簡單了,經常在用,就是這兩個全域性方法:`parseInt`和`parseFloat`。 ### 物件轉字串 物件轉換為字串和數值會稍微麻煩點,下面我們單獨來探究下。物件轉為字串主要有三種方法: 1. `value.toString()` 這個前面講過了 2. `'' + value`。這個是前面提到過的隱式轉換,但是`value`是物件的話會按照下面的順序進行轉換: 1. 先呼叫`value.valueOf`方法,如果值是原始值,則返回 2. 否則呼叫`value.toString`方法,如果值是原始值,則返回 3. 否則報錯TypeError 3. `String(value)`。這個是前面提到的顯式轉換,流程跟前面類似,但是呼叫`toString`和`valueOf`的順序不一樣。 1. 先呼叫`value.toString`方法,如果值是原始值,則返回 2. 否則呼叫`value.valueOf`方法,如果值是原始值,則返回 3. 否則報錯TypeError 需要注意的是,`Date`物件有點特殊,他始終呼叫`toString`方法。 下面我們寫一段程式碼來驗證下: ```javascript Object.prototype.valueOf = function() { return 'aaa'; } Object.prototype.toString = function() { return 'bbb'; } let a = {}; let b = '' + a; let c = String(a); console.log(b); console.log(c); ``` 上述程式碼輸出是,跟我們預期一樣: ![image-20200506160225229](https://user-gold-cdn.xitu.io/2020/5/6/171ea4dceaf95808?w=346&h=235&f=png&s=21804) ### 物件轉數值 物件型別轉為數值主要有兩種方法: 1. `+value` 2. `Number(value)` 這兩種的執行邏輯是一樣的: 1. 先呼叫`valueOf`方法,如果值是原始值,就返回 2. 否則,呼叫`toString`方法,然後將`toString`的返回值轉換為數值 照例寫個例子看下: ```javascript Object.prototype.valueOf = function() { return {}; } Object.prototype.toString = function() { return 'bbb'; } let a = {}; let b = +a; let c = Number(a); console.log(b); console.log(c); ``` 上述程式碼的輸出都是`NaN`,這是因為我們`toString`方法返回的`bbb`沒辦法轉化為正常數值,強行轉就是`NaN`: ![image-20200506160750545](https://user-gold-cdn.xitu.io/2020/5/6/171ea4e0cbe2d251?w=358&h=240&f=png&s=21051) ## 型別檢測 型別檢測是我們經常遇到的問題,面試時也經常問到各種型別檢測的方法,下面是幾種常用的型別檢測的方法。 ### typeof 做型別檢測最常用的就是`typeof`了: ```javascript let a; typeof a; // undefined let b = true; typeof b; // boolean let c = 123; typeof c; // number let d = 'abc'; typeof d; // string let e = () => {}; typeof e; // function let f = {}; typeof f; // object let g = Symbol(); typeof g; // symbol ``` ### instanceof `typeof`最簡單,但是他只能判斷基本的型別,如果是物件的話,沒法判斷具體是哪個物件。`instanceof`可以檢測一個物件是不是某個類的例項,這種檢測其實基於面向物件和原型鏈的,[更多關於instanceof原理的可以看這篇文章](https://juejin.im/post/5e50e5b16fb9a07c9a1959af#heading-8)。下面來看個例子: ```javascript let a = new Date(); a instanceof Date; // true ``` ### constructor `constructor`的原理其實跟前面的`instanceof`有點像,也是基於面向物件和原型鏈的。一個物件如果是一個類的例項的話,那他原型上的`constructor`其實也就指向了這個類,我們可以通過判斷他的`constructor`來判斷他是不是某個類的例項。[具體的原理在前面提到的文章也有詳細說明](https://juejin.im/post/5e50e5b16fb9a07c9a1959af#heading-4)。還是用上面那個例子: ```javascript let a = new Date(); a.constructor === Date; // true ``` 使用`constructor`判斷的時候要注意,如果原型上的`constructor`被修改了,這種檢測可能就失效了,比如: ```javascript function a() {} a.prototype = { x: 1 } let b = new a(); b.constructor === a; // 注意這時候是 false ``` 上面為`false`的原因是,`constructor`這個屬性其實是掛在`a.prototype`下面的,我們在給`a.prototype`賦值的時候其實覆蓋了之前的整個`prototype`,也覆蓋了`a.prototype.constructor`,這時候他其實壓根就沒有這個屬性,如果我們非要訪問這個屬性,只能去原型鏈上找,這時候會找到`Object`: ![image-20200506172606821](https://user-gold-cdn.xitu.io/2020/5/6/171ea4e5361ba4a7?w=319&h=114&f=png&s=15266) 要避免這個問題,我們在給原型新增屬性時,最好不要整個覆蓋,而是隻新增我們需要的屬性,上面的改為: ```javascript a.prototype.x = 1; ``` 如果一定要整個覆蓋,記得把`constructor`加回來: ```javascript a.prototype = { constructor: a, x: 1 } ``` ### duck-typing `duck-typing`翻譯叫“鴨子型別”,名字比較奇怪,意思是指一個動物,如果看起來像鴨子,走起路來像鴨子,叫起來也像鴨子,那我們就認為他是隻鴨子。就是說我們通過他的外觀和行為來判斷他是不是鴨子,而不是準確的去檢測他的基因是不是鴨子。這種方式在科學上當然是不嚴謹的,但是在部分場景下卻是有效的。用程式語言來說,就是看某個物件是不是具有某些特定的屬性和方法,來確定他是不是我們要的物件。比如有些開源庫判斷一個物件是不是陣列會有下面的寫法: ```javascript function isArray(object) { return object !== null && typeof object === 'object' && 'splice' in object && 'join' in object } isArray([]); // true ``` 這就是通過檢測目標物件是不是包含Array應該有的方法來判斷他是不是一個Array。這就是所謂的看著像鴨子,那就是鴨子。但是一個具有`splice`和`join`方法的物件也能通過這個檢測,所以這樣是不準確的,只是部分場景適用。 ### Object.prototype.toString.call `Object.prototype.toString.call`是比較準確的,可以用來判斷原生物件具體是哪個型別: ```javascript Object.prototype.toString.call(new Array()); // [object Array] Object.prototype.toString.call(new Date()); // [object Date] ``` 這個方法返回的是`[object XXX]`,這個XXX是對應的建構函式名字。但是他只能檢測原生物件,對於自定義型別是沒有用的: ```javascript function a() {} let b = new a(); Object.prototype.toString.call(b); // [object Object] ``` 可以看到對於自定義類`a`的例項`b`,我們得到仍然是`[object Object]`,而不是我們預期的`[object a]`。 ### 一些原生方法: Array.isArray,Number.isInteger JS為了解決型別檢測的問題,也引入了一些原生方法來提供支援,比如`Array.isArray`和`Number.isInteger`等。`Array.isArray`可以用來檢測一個物件是不是陣列: ```javascript Array.isArray([]); // true Array.isArray(123); // false ``` `Number.isInteger`可以用來檢測一個物件是不是整數: ```javascript Number.isInteger(1); // true Number.isInteger(-1); // true Number.isInteger(-1.1); // false Number.isInteger('aaa'); // false ``` 如果有原生檢測的方法我們當然推薦使用原生方法了,但是目前原生方法並沒有那麼多和全面,很多時候還是要用前面的方法來檢測型別。 ### 小節 JS其實沒有一種完美的方法來檢測所有的型別,具體的檢測方法需要我們根據實際情況來進行選擇和取捨。下面是幾種方法的總結: ![image-20200506180011564](https://user-gold-cdn.xitu.io/2020/5/6/171ea4e9492f6b7a?w=1712&h=980&f=png&s=212928) ## 總結 1. JS有兩種資料型別,原始型別和引用型別,引用型別主要就是物件。 2. 當我們使用`+`,邏輯判斷或者`==`時會有隱式的型別轉換。 3. 有時候隱式的型別轉換會出現我們不想要的結果,如果我們確定要進行判斷或者型別轉換,最好使用顯式的,比如使用`===`,而不是`==`。 4. 物件轉為字串和數值可能需要調`valueOf`和`toString`方法,呼叫順序需要看具體場景。 5. JS沒有一個完美的型別檢測方法,我們最好根據需要選擇具體的檢測方法。 **文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。** **作者博文GitHub專案地址: [https://github.com/dennis-jiang/Front-End-Knowledges](https://github.com/dennis-jiang/Front-End-Knowledges)** **作者掘金文章彙總:[https://juejin.im/post/5e3ffc85518825494e2772fd](https://juejin.im/post/5e3ffc85518825494e27