因为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/