因爲JavaScript是門鬆散類型語言,定義變量時沒有類型標識信息,而且在運行期能夠動態更改其類型,因此一個變量的類型在運行期是不可預測的,所以,數據類型檢測在開發當中就成爲一個必需要了解和掌握的知識點。javascript
對於數據類型檢測,實習新手會用typeof,老司機會用Object.prototype.toString.call();,在實際開發當中,後者能夠說是目前比較好的辦法了,能夠準確地檢測幾種常見的內置類型,要比typeof靠譜得多。那麼究竟類型檢測都有哪些方法,各自都有哪些優劣呢,博主就藉此機會來聊一聊數據類型檢測的一些方法和其中的細節原理。html
最先咱們就使用下面的typeof方式檢測一個值的類型:java
var foo = 3; var type = typeof foo; // 或者 var type = typeof(foo);
後者看上去好像是一個函數調用,不過須要注意的是,typeof只是一個操做符,而不是函數,typeof後面的括號也不是函數調用,而是一個強制運算求值表達式,就至關於數學運算中的括號同樣,最終返回一個運算結果,咱們將上面的表達式分開就容易理解了:數組
var type = typeof (foo);
上面咱們介紹到,初學者會用typeof判斷一個值的類型,而老手們都踩過它的坑:數據結構
// 下面幾個能夠檢測出準確的類型 typeof 3; // 'number' typeof NaN; // 'number' typeof '3'; // 'string' typeof ''; // 'string' typeof true; // 'boolean' typeof Boolean(false); // 'boolean' typeof undefined; // 'undefined' typeof {}; // 'object' typeof function fn() {}; // 'function' // ES6中的Symbol類型 typeof Symbol(); // 'symbol' // ES6中的類本質上仍是函數 typeof class C {}; // 'function' // 如下均不能檢測出準確的類型 typeof null; // 'object' typeof new Number(3); // 'object' typeof new String('3'); // 'object' typeof new Boolean(true); // 'object' typeof []; // 'object' typeof /\w+/; // 'object' typeof new Date(); // 'object' typeof new Error(); // 'object' // ES6中的Map和Set typeof new Map(); // 'object' typeof new Set(); // 'object'
能夠看到,對於基礎類型,typeof仍是比較準確的,而基礎類型的包裝類型就沒法正確地檢測了,只是籠統地返回一個'object',而對於ES6新添加的基礎類型Symbol和數據結構對象Map&Set,也分別返回相應的類型值,但其中的Map和Set也是沒法使用typeof檢測其類型的。框架
Object和Function能夠給出正確的類型結果,而其餘像Array、RegExp、Date以及Error類型,沒法獲得正確的結果,一樣只是獲得了一個'object',這是因爲它們都是繼承自Object,其結果是,typeof操做符將Object和這幾個類型混爲一類,咱們沒有辦法將他們區分開來。函數
比較特殊的是null,因爲它是空對象的標記,因此會返回一個'object',站在開發者角度,這是徹底錯誤的。最後須要注意的是,上面的NaN雖然是Not a Number,但它的確屬於Number類型,這個有點滑稽,不過一般咱們會用isNaN()方法來檢測一個值是否爲NaN。spa
因此,僅靠typeof是不能檢測出以上全部類型,對於'object'的結果,一般咱們都須要進一步的檢測,以區分開不一樣的對象類型。目前流行的框架中,也不乏typeof的使用,因此說typeof並不是一無可取,而是要適當地使用。prototype
除了上面的typeof,還可使用值的構造器,也就是利用constructor屬性來檢測其類型:code
(3).constructor === Number; // true NaN.constructor === Number; // true ''.constructor === String; // true true.constructor === Boolean; // true Symbol().constructor === Symbol; // true var o = {}; o.constructor === Object; // true var fn = function() {}; fn.constructor === Function; // true var ary = []; ary.constructor === Array; // true var date = new Date(); date.constructor === Date; // true var regex = /\w+/; regex.constructor === RegExp; // true var error = new Error(); error.constructor === Error; // true var map = new Map(); map.constructor === Map; // true var set = new Set(); set.constructor === Set; // true
從上面的結果來看,利用constructor屬性確實能夠檢測大部分值的類型,對於基礎類型也一樣管用,那爲何基礎類型也有構造器呢,這裏實際上是對基礎類型進行了隱式包裝,引擎檢測到對基礎類型進行屬性的存取時,就對其進行自動包裝,轉爲了對應的包裝類型,因此上面的基礎類型結果最終的代碼邏輯爲:
new Number(3).constructor === Number; // true new Number(NaN).constructor === Number; // true new String('').constructor === String; // true new Boolean(true).constructor === Boolean; // true
須要注意的是,咱們對基礎類型的數字3進行屬性的存取時,使用了一對括號,這是由於,若是省略了括號而直接使用3.constructor,引擎會嘗試解析一個浮點數,所以會引起一個異常。另外,咱們沒有列舉null和undefined的例子,這是由於,null和undefined沒有對應的包裝類型,所以沒法使用constructor進行類型檢測,嘗試訪問constructor會引起一個異常,因此,constructor沒法識別null和undefined。不過咱們能夠先利用其餘手段檢測null和undefined,而後對其餘類型使用構造器檢測,就像下面這樣:
/** * 利用contructor檢測數據類型 */ function is(value, type) { // 先處理null和undefined if (value == null) { return value === type; } // 而後檢測constructor return value.constructor === type; } var isNull = is(null, null); // true var isUndefined = is(undefined, undefined); // true var isNumber = is(3, Number); // true var isString = is('3', String); // true var isBoolean = is(true, Boolean); // true var isSymbol = is(Symbol(), Symbol); // true var isObject = is({}, Object); // true var isArray = is([], Array); // true var isFunction = is(function(){}, Function); // true var isRegExp = is(/./, RegExp); // true var isDate = is(new Date, Date); // true var isError = is(new Error, Error); // true var isMap = is(new Map, Map); // true var isSet = is(new Set, Set); // true
除了上面的常規類型,咱們還可使用它檢測自定義對象類型:
function Animal() {} var animal = new Animal(); var isAnimal = is(animal, Animal); // true
可是涉及到對象的繼承時,構造器檢測也有些力不從心了:
function Tiger() {} Tiger.prototype = new Animal(); Tiger.prototype.constructor = Tiger; var tiger = new Tiger(); var isAnimal = is(tiger, Animal); // false
咱們也看到了,在上面的對象繼承中,Tiger原型中的構造器被從新指向了本身,因此咱們沒有辦法檢測到它是否屬於父類類型。一般這個時候,咱們會使用instanceof操做符:
var isAnimal = tiger instanceof Animal; // true
instanceof也能夠檢測值的類型,但這僅限於對象類型,而對於對象類型以外的值來講,instanceof並無什麼用處。undefined顯然沒有對應的包裝類型,null雖然也被typeof劃分爲'object',但它並非Object的實例,而對於基礎類型,instanceof並不會對其進行自動包裝:
// 雖然typeof null的結果爲'object' 但它並非Object的實例 null instanceof Object; // false // 對於基礎類型 instanceof操做符並不會有隱式包裝 3 instanceof Number; // false '3' instanceof Number; // false true instanceof Boolean; // false Symbol() instanceof Symbol; // false // 只對對象類型起做用 new Number(3) instanceof Number; // true new String('3') instanceof String; // true new Boolean(true) instanceof Boolean; // true Object(Symbol()) instanceof Symbol; // true ({}) instanceof Object; // true [] instanceof Array; // true (function(){}) instanceof Function; // true /./ instanceof RegExp; // true new Date instanceof Date; // true new Error instanceof Error; // true new Map instanceof Map; // true new Set instanceof Set; // true
很遺憾,咱們沒有辦法使用instanceof來檢測基礎類型的值了,若是非要使用,前提是先要將基礎類型包裝成對象類型,這樣一來就必須使用其餘方法檢測到這些基礎類型,而後進行包裝,這樣作毫無心義,由於咱們已經獲取到它們的類型了。因此,除了對象類型以外,不要使用instanceof操做符來檢測類型。
最後來講一說如何利用Object中的toString()方法來檢測數據類型。一般,咱們會使用下面兩種形式獲取到Object的toString方法:
var toString = ({}).toString; // 或者 var toString = Object.prototype.toString;
推薦使用後者,獲取對象原型中的toString()方法。下面咱們來看看它是如何獲取到各類值的類型的:
toString.call(undefined); // '[object Undefined]' toString.call(null); // '[object Null]' toString.call(3); // '[object Number]' toString.call(true); // '[object Boolean]' toString.call(''); // '[object String]' toString.call(Symbol()); // '[object Symbol]' toString.call({}); // '[object Object]' toString.call([]); // '[object Array]' toString.call(function(){}); // '[object Function]' toString.call(/\w+/); // '[object RegExp]' toString.call(new Date); // '[object Date]' toString.call(new Error); // '[object Error]' toString.call(new Map); // '[object Map]' toString.call(new Set); // '[object Set]'
從代碼中能夠看到,不論是基礎類型仍是對象類型,均會的到一個包含其類型的字符串,null和undefined也不例外,它們看上去好像有了本身的「包裝類型」,爲何Object中的toString()方法這麼神奇呢,歸根結底,這都是ECMA規範規定的,從來的規範中都對這個方法有所定義,而最爲詳盡的,當屬最新的ES6規範了,下面是ES6中關於Object原型中toString()方法的規定:
其主要處理方式爲:若是上下文對象爲null和undefined,返回"[object Null]"和"[object Undefined]",若是是其餘值,先將其轉爲對象,而後一次檢測數組、字符串、arguments對象、函數及其它對象,獲得一個內建的類型標記,最後拼接成"[object Type]"這樣的字符串。
看上去這個方法至關的可靠,利用它,咱們就能夠把它們當成普通基礎類型一塊兒處理了,下面咱們封裝一個函數,用於判斷常見類型:
// 利用Object#toString()檢測類型 var _toString = Object.prototype.toString; function is(value, typeString) { // 獲取到類型字符串 var stripped = _toString.call(value).replace(/^\[object\s|\]$/g, ''); return stripped === typeString; } is(null, 'Null'); // true is(undefined, 'Undefined'); // true is(3, 'Number'); // true is(true, 'Boolean'); // true is('hello', 'String'); // true is(Symbol(), 'Symbol'); // true is({}, 'Object'); // true is([], 'Array'); // true is(function(){}, 'Function'); // true is(/\w+/, 'RegExp'); // true is(new Date, 'Date'); // true is(new Error, 'Error'); // true is(new Map, 'Map'); // true is(new Set, 'Set'); // true
雖然上面常見類型都能被正確識別,但Object#toString()方法也不是萬能的,它不能檢測自定義類型:
function Animal() {} var animal = new Animal(); var isAnimal = is(animal, 'Animal'); // false ({}).toString.call(animal); // '[object Object]'
從這一點來看,相比較constructor方式也還有點遜色,因此Object#toString()方法也不是萬能的,遇到自定義類型時,咱們仍是得依賴instanceof來檢測。
上面介紹了這麼多,整體來說,能夠概括爲下面幾點:
1. Object#toString()和改進後的constructor方式覆蓋的類型較多,比較實用
2. 若是要檢測一個變量是否爲自定義類型,要使用instanceof操做符
3. 也能夠有選擇地使用typeof操做符,但不要過度依賴它
本文完。
參考資料:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
http://www.ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring
http://tobyho.com/2011/01/28/checking-types-in-javascript/