JS做型別檢測到底有幾種方法?看完本文就知道了!
阿新 • • 發佈:2020-05-09
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