JS做類型檢測到底有幾種方法?看完本文就知道了!
- 2020 年 5 月 9 日
- 筆記
- javascript, 前端
JS有很多數據類型,對於不同數據類型的識別和相互轉換也是面試中的一個常考點,本文主要講的就是類型轉換和類型檢測。
數據類型
JS中的數據類型主要分為兩大類:原始類型(值類型)和引用類型。常見的數據類型如下圖所示:
原始數據類型存在棧中,引用類型在棧中存的是一個引用地址,這個地址指向的是堆中的一個數據對象。需要注意的是null在這裡我們算在原始類型裡面,但是你用typeof的時候會發現他是object,原因是就算他是一個對象,那他應該在棧中存一個引用地址,但是他是一個空對象,所以這個地址為空,也就是不對應堆中的任意一個數據,他在堆中沒有數據,只存在於棧中,所以這裡算為了原始類型。引用類型其實主要就是Object,Array和Function這些其實也都是Object派生出來的。關於這兩種類型在記憶體中的更詳細的知識可以看這篇文章。
下面我們來看看這兩種類型的區別:
原始類型
-
原始類型的值無法更改,要更改只能重新賦值。像下面這樣嘗試去修改是不行的,但是整個重新賦值可以。
-
原始類型的比較就是比較值,值相等,他們就相等
引用類型
-
引用類型的值是可以修改的,注意這個時候我們雖然修改了
a裡面的屬性,但是a在棧上的引用地址並沒有變化,變化的是堆中的數據。 -
引用類型的比較是比較他們的索引地址,而不是他們的值。比如下面兩個對象,看著是一樣的,但是他們的引用地址不一樣,其實是不等的:
要想讓他們相等,得直接將
b賦值為a,這樣他們的引用地址一樣,就是相等的。
類型轉換
JS中當不同類型的數據進行計算的時候會進行類型轉換,比如下面的例子:
上面的例子中,我們用了加減來操作幾個非數字的類型,這時候JS會進行隱式的類型轉換,然後再進行加減運算。除了JS本身的隱式轉換外,有時候我們還會主動進行類型轉換,這就算是顯示類型轉換了。
隱式類型轉換
轉為字元串
經常出現在+運算中,並且其中有一個操作數不是數值類型
let s = 4 + 'px' + 5;
console.log(s); // 4px5
s = 123e-2 + 'a';
console.log(s); // 1.23a
轉為數值
經常出現在數學運算中,表示連接字元串的+運算除外
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或者邏輯運算中
let s = 'abc';
if(s) {
console.log(s); // abc
}
console.log(!!s); // true
下面的值在進行bool轉換時會轉換為false,除此以外都是true:
- 0
- NaN
- ''(空字元串)
- null
- undefined
==運算符
當我們使用==進行比較時,如果兩邊的類型不同,JS會進行類型轉換,然後再比較,===則不會進行類型轉換,如果===兩邊的數據類型不同,直接返回false。
上面只是列舉了其中幾種情況,更多的情況可以參考下面這種表,這個表來自於MDN。這個表的內容比較多,有些是規範直接定義的,比如null == undefined,也沒有太多邏輯可言。我們不確定時可以來查下這個表,但是實際開發中其實是不建議使用==的,因為如果你把這個轉換關係記錯了的話可能就會引入比較難排查的bug,一般推薦直接使用===。
轉換規則
下面這幾張表是一些轉換規則,來自於《JS權威指南》:
顯式類型轉換
顯式類型轉換是我們自己寫程式碼明確轉換的類型,可以使程式碼看起來更清晰,是實際開發時推薦的做法。
轉字元串
顯式轉換為字元串可以使用toString方法,它的執行結果通常和String()方法一致。Number類型的toString方法還支援參數,可以指定需要轉換的進位。下面的圖是一些原始類型的toString(),null和undefined沒有toString方法,調用會報錯:
Number類型的toString方法支援進位:
轉數值
轉為數值就很簡單了,經常在用,就是這兩個全局方法:parseInt和parseFloat。
對象轉字元串
對象轉換為字元串和數值會稍微麻煩點,下面我們單獨來探究下。對象轉為字元串主要有三種方法:
-
value.toString()這個前面講過了
-
'' + value。這個是前面提到過的隱式轉換,但是value是對象的話會按照下面的順序進行轉換:- 先調用
value.valueOf方法,如果值是原始值,則返回 - 否則調用
value.toString方法,如果值是原始值,則返回 - 否則報錯TypeError
- 先調用
-
String(value)。這個是前面提到的顯式轉換,流程跟前面類似,但是調用toString和valueOf的順序不一樣。- 先調用
value.toString方法,如果值是原始值,則返回 - 否則調用
value.valueOf方法,如果值是原始值,則返回 - 否則報錯TypeError
- 先調用
需要注意的是,Date對象有點特殊,他始終調用toString方法。
下面我們寫一段程式碼來驗證下:
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);
上述程式碼輸出是,跟我們預期一樣:
對象轉數值
對象類型轉為數值主要有兩種方法:
+valueNumber(value)
這兩種的執行邏輯是一樣的:
- 先調用
valueOf方法,如果值是原始值,就返回 - 否則,調用
toString方法,然後將toString的返回值轉換為數值
照例寫個例子看下:
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:
類型檢測
類型檢測是我們經常遇到的問題,面試時也經常問到各種類型檢測的方法,下面是幾種常用的類型檢測的方法。
typeof
做類型檢測最常用的就是typeof了:
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原理的可以看這篇文章。下面來看個例子:
let a = new Date();
a instanceof Date; // true
constructor
constructor的原理其實跟前面的instanceof有點像,也是基於面向對象和原型鏈的。一個對象如果是一個類的實例的話,那他原型上的constructor其實也就指向了這個類,我們可以通過判斷他的constructor來判斷他是不是某個類的實例。具體的原理在前面提到的文章也有詳細說明。還是用上面那個例子:
let a = new Date();
a.constructor === Date; // true
使用constructor判斷的時候要注意,如果原型上的constructor被修改了,這種檢測可能就失效了,比如:
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:
要避免這個問題,我們在給原型添加屬性時,最好不要整個覆蓋,而是只添加我們需要的屬性,上面的改為:
a.prototype.x = 1;
如果一定要整個覆蓋,記得把constructor加回來:
a.prototype = {
constructor: a,
x: 1
}
duck-typing
duck-typing翻譯叫「鴨子類型」,名字比較奇怪,意思是指一個動物,如果看起來像鴨子,走起路來像鴨子,叫起來也像鴨子,那我們就認為他是只鴨子。就是說我們通過他的外觀和行為來判斷他是不是鴨子,而不是準確的去檢測他的基因是不是鴨子。這種方式在科學上當然是不嚴謹的,但是在部分場景下卻是有效的。用程式語言來說,就是看某個對象是不是具有某些特定的屬性和方法,來確定他是不是我們要的對象。比如有些開源庫判斷一個對象是不是數組會有下面的寫法:
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是比較準確的,可以用來判斷原生對象具體是哪個類型:
Object.prototype.toString.call(new Array()); // [object Array]
Object.prototype.toString.call(new Date()); // [object Date]
這個方法返回的是[object XXX],這個XXX是對應的構造函數名字。但是他只能檢測原生對象,對於自定義類型是沒有用的:
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可以用來檢測一個對象是不是數組:
Array.isArray([]); // true
Array.isArray(123); // false
Number.isInteger可以用來檢測一個對象是不是整數:
Number.isInteger(1); // true
Number.isInteger(-1); // true
Number.isInteger(-1.1); // false
Number.isInteger('aaa'); // false
如果有原生檢測的方法我們當然推薦使用原生方法了,但是目前原生方法並沒有那麼多和全面,很多時候還是要用前面的方法來檢測類型。
小節
JS其實沒有一種完美的方法來檢測所有的類型,具體的檢測方法需要我們根據實際情況來進行選擇和取捨。下面是幾種方法的總結:
總結
- JS有兩種數據類型,原始類型和引用類型,引用類型主要就是對象。
- 當我們使用
+,邏輯判斷或者==時會有隱式的類型轉換。 - 有時候隱式的類型轉換會出現我們不想要的結果,如果我們確定要進行判斷或者類型轉換,最好使用顯式的,比如使用
===,而不是==。 - 對象轉為字元串和數值可能需要調
valueOf和toString方法,調用順序需要看具體場景。 - JS沒有一個完美的類型檢測方法,我們最好根據需要選擇具體的檢測方法。
文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。
作者博文GitHub項目地址: //github.com/dennis-jiang/Front-End-Knowledges
作者掘金文章匯總://juejin.im/post/5e3ffc85518825494e2772fd

