由於JavaScript是門松散類型語言,定義變量時沒有類型標識信息,並且在運行期可以動態更改其類型,所以一個變量的類型在運行期是不可預測的,因此,數據類型檢測在開發當中就成為一個必須要了解和掌握的知識點。
對於數據類型檢測,實習新手會用typeof,老司機會用Object.prototype.toString.call();,在實際開發當中,后者可以說是目前比較好的辦法了,可以准確地檢測幾種常見的內置類型,要比typeof靠譜得多。那么究竟類型檢測都有哪些方法,各自都有哪些優劣呢,博主就借此機會來聊一聊數據類型檢測的一些方法和其中的細節原理。
最早我們就使用下面的typeof方式檢測一個值的類型:
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。
所以,僅靠typeof是不能檢測出以上所有類型,對於'object'的結果,通常我們都需要進一步的檢測,以區分開不同的對象類型。目前流行的框架中,也不乏typeof的使用,所以說typeof並非一無是處,而是要適當地使用。
除了上面的typeof,還可以使用值的構造器,也就是利用constructor屬性來檢測其類型:
(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/
