JavaScript: 数据类型检测
由于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/