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

JS有很多數據類型,對於不同數據類型的識別和相互轉換也是面試中的一個常考點,本文主要講的就是類型轉換和類型檢測。

數據類型

JS中的數據類型主要分為兩大類:原始類型(值類型)和引用類型。常見的數據類型如下圖所示:

image-20200506103537269

原始數據類型存在棧中,引用類型在棧中存的是一個引用地址,這個地址指向的是堆中的一個數據對象。需要注意的是null在這裡我們算在原始類型裡面,但是你用typeof的時候會發現他是object,原因是就算他是一個對象,那他應該在棧中存一個引用地址,但是他是一個空對象,所以這個地址為空,也就是不對應堆中的任意一個數據,他在堆中沒有數據,只存在於棧中,所以這裡算為了原始類型。引用類型其實主要就是Object,Array和Function這些其實也都是Object派生出來的。關於這兩種類型在記憶體中的更詳細的知識可以看這篇文章。

image-20200506104330145

下面我們來看看這兩種類型的區別:

原始類型

  1. 原始類型的值無法更改,要更改只能重新賦值。像下面這樣嘗試去修改是不行的,但是整個重新賦值可以。

    image-20200506104958681

    image-20200506105044457

  2. 原始類型的比較就是比較值,值相等,他們就相等

引用類型

  1. 引用類型的值是可以修改的,注意這個時候我們雖然修改了a裡面的屬性,但是a在棧上的引用地址並沒有變化,變化的是堆中的數據。

    image-20200506105513907

  2. 引用類型的比較是比較他們的索引地址,而不是他們的值。比如下面兩個對象,看著是一樣的,但是他們的引用地址不一樣,其實是不等的:

    image-20200506110135018

    要想讓他們相等,得直接將b賦值為a,這樣他們的引用地址一樣,就是相等的。

    image-20200506110256501

類型轉換

JS中當不同類型的數據進行計算的時候會進行類型轉換,比如下面的例子:

image-20200506110621714

上面的例子中,我們用了加減來操作幾個非數字的類型,這時候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:

  1. 0
  2. NaN
  3. ''(空字元串)
  4. null
  5. undefined

==運算符

當我們使用==進行比較時,如果兩邊的類型不同,JS會進行類型轉換,然後再比較,===則不會進行類型轉換,如果===兩邊的數據類型不同,直接返回false。

image-20200506112606774

上面只是列舉了其中幾種情況,更多的情況可以參考下面這種表,這個表來自於MDN。這個表的內容比較多,有些是規範直接定義的,比如null == undefined,也沒有太多邏輯可言。我們不確定時可以來查下這個表,但是實際開發中其實是不建議使用==的,因為如果你把這個轉換關係記錯了的話可能就會引入比較難排查的bug,一般推薦直接使用===。

image-20200506111718423

轉換規則

下面這幾張表是一些轉換規則,來自於《JS權威指南》:

image-20200505185955549

image-20200505190049837

image-20200505190124871

顯式類型轉換

顯式類型轉換是我們自己寫程式碼明確轉換的類型,可以使程式碼看起來更清晰,是實際開發時推薦的做法。

image-20200506113002845

轉字元串

顯式轉換為字元串可以使用toString方法,它的執行結果通常和String()方法一致。Number類型的toString方法還支援參數,可以指定需要轉換的進位。下面的圖是一些原始類型的toString(),null和undefined沒有toString方法,調用會報錯:

image-20200506113217062

Number類型的toString方法支援進位:

image-20200506113346662

轉數值

轉為數值就很簡單了,經常在用,就是這兩個全局方法: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方法。

下面我們寫一段程式碼來驗證下:

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

對象轉數值

對象類型轉為數值主要有兩種方法:

  1. +value
  2. Number(value)

這兩種的執行邏輯是一樣的:

  1. 先調用valueOf方法,如果值是原始值,就返回
  2. 否則,調用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:

image-20200506160750545

類型檢測

類型檢測是我們經常遇到的問題,面試時也經常問到各種類型檢測的方法,下面是幾種常用的類型檢測的方法。

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:

image-20200506172606821

要避免這個問題,我們在給原型添加屬性時,最好不要整個覆蓋,而是只添加我們需要的屬性,上面的改為:

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其實沒有一種完美的方法來檢測所有的類型,具體的檢測方法需要我們根據實際情況來進行選擇和取捨。下面是幾種方法的總結:

image-20200506180011564

總結

  1. JS有兩種數據類型,原始類型和引用類型,引用類型主要就是對象。
  2. 當我們使用+,邏輯判斷或者==時會有隱式的類型轉換。
  3. 有時候隱式的類型轉換會出現我們不想要的結果,如果我們確定要進行判斷或者類型轉換,最好使用顯式的,比如使用===,而不是==。
  4. 對象轉為字元串和數值可能需要調valueOf和toString方法,調用順序需要看具體場景。
  5. JS沒有一個完美的類型檢測方法,我們最好根據需要選擇具體的檢測方法。

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。

作者博文GitHub項目地址: //github.com/dennis-jiang/Front-End-Knowledges

作者掘金文章匯總://juejin.im/post/5e3ffc85518825494e2772fd