由于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/

http://javascript.crockford.com/remedial.html

http://javascript.info/tutorial/type-detection

JavaScript系列文章:不能不看的数据类型检测的更多相关文章

  1. JavaScript系列文章:谈谈let和const

    JavaScript系列文章:谈谈let和const   最近接触到ES6的一些相关新特性,想借let和const两个命令谈谈JavaScript在变量方面的改进. 由于let和const有很多相似之 ...

  2. JavaScript系列文章:自动类型转换

    我们都知道,JavaScript是类型松散型语言,在声明一个变量时,我们是无法明确声明其类型的,变量的类型是根据其实际值来决定的,而且在运行期间,我们可以随时改变这个变量的值和类型,另外,变量在运行期 ...

  3. 【转】JavaScript系列文章:自动类型转换

    我们都知道,JavaScript是类型松散型语言,在声明一个变量时,我们是无法明确声明其类型的,变量的类型是根据其实际值来决定的,而且在运行期间,我们可以随时改变这个变量的值和类型,另外,变量在运行期 ...

  4. JavaScript系列文章:详解正则表达式之二

    在上一篇文章中我们讲了正则表达式的基本用法,接下来博主想聊聊其中的细节,今天就从正则修饰符开始吧. 正则修饰符又称为正则标记(flags),它会对正则的匹配规则做限定,进而影响匹配的最终结果.在上次的 ...

  5. JavaScript系列文章:变量提升和函数提升

    第一篇文章中提到了变量的提升,所以今天就来介绍一下变量提升和函数提升.这个知识点可谓是老生常谈了,不过其中有些细节方面博主很想借此机会,好好总结一下. 今天主要介绍以下几点: 1. 变量提升 2. 函 ...

  6. JavaScript系列文章:详解正则表达式之三

    在上两篇文章中博主介绍了JavaScript中的正则常用方法和正则修饰符,今天准备聊一聊元字符和高级匹配的相关内容. 首先说说元字符,想必大家也都比较熟悉了,JS中的元字符有以下几种: / \ | . ...

  7. JavaScript系列文章:详解正则表达式之一

    正则表达式是一个精巧的利器,经常用来在字符串中查找和替换,JavaScript语言参照Perl,也提供了正则表达式相关模块,开发当中非常实用,在一些类库或是框架中,比如jQuery,就存在大量的正则表 ...

  8. JavaScript系列文章:自动类型转换-续

    在上一篇文章中,我们详细讲解了JavaScript中的自动类型转换,由于篇幅限制,没能覆盖到所有的转换规则,这次准备详细讲解一下. 上次我们提到了对象类型参与运算时转换规则: 1). 在逻辑环境中执行 ...

  9. JavaScript系列文章:从let和const谈起

    注册博客园账号也有好些年了,有事没事经常来逛逛,感觉博客园的同学们一直都很活跃,相比国内其他社区来讲,这里的技术氛围很浓,非常适合学习和交流,所以博主我也决定在这里驻扎了,在这里,博主希望能坚持写一些 ...

随机推荐

  1. 记处理线上记录垃圾日志 The view 'Error' or its master was not found

    最近监控线上日志,网站是ASP.NET MVC 开发的,发现不少错误日志都记录同样的内容: The view 'Error' or its master was not found or no vie ...

  2. iOS - GitHub干货分享(APP引导页的高度集成 - DHGuidePageHUD - ②)

    距上一篇博客"APP引导页的高度集成 - DHGuidePageHUD - ①"的发布有一段时间了, 后来又在SDK中补充了一些新的内容进去但是一直没来得及跟大家分享, 今天来跟大 ...

  3. iOS 自定义方法 - UIView扩展

    示例代码 //#import <UIKit/UIKit.h>@interface UIView (LPCView)/** 上 */@property CGFloat top;/** 下 * ...

  4. Laravel大型项目系列教程(一)

    Laravel大型项目系列教程(一) 一.课程概述 1.课程介绍 本教程将使用Laravel完成一个多用户的博客系统,大概会包含如下内容: 路由管理. 用户管理,如用户注册.修改信息.锁定用户等. 文 ...

  5. 由一个bug引发的SQLite缓存一致性探索

    问题 我们在生产环境中使用SQLite时中发现建表报“table xxx already exists”错误,但DB文件中并没有该表.后面才发现这个是SQLite在实现过程中的一个bug,而这个bug ...

  6. Oracle数据库,数据的增、删、改、查

    oracle数据库中,数据的增.删.改.查,通过SQL语句实现 SQL:结构化查询语言: 特点:不区分大小写:字符串用单引号引起来:语句结束用分号表示结束: 行注释,在语句的最前面加"--& ...

  7. ASP.NET Cookie(一)--基本应用

    Cookie提供了一种在Web应用程序中存储用户特定信息的方法.例如,当用户访问您的站点时,您可以使用Cookie存储用户首选项或其他信息.当该用户再次访问您的网站时,应用程序便可以检索以前存储的信息 ...

  8. 阿里云ECS服务器配置(Ubuntu+JAVA+Tomcat+Mysql)

    最近购买了阿里云的ECS服务器,就服务器的安装配置做简要的说明,也方便日后查看. 1.远程操作服务器 远程操作服务器可以使用putty工具,下载地址:http://pan.baidu.com/s/1q ...

  9. [python]获取网页中内容为汉字的字符串的判断

    实际上是这样,将获取到网页中表单内容与汉字字符串作比较,即: a = request.POST['a'] if a == '博客园': print 'ok' else: print 'false' a ...

  10. BZOJ 2434: [Noi2011]阿狸的打字机 [AC自动机 Fail树 树状数组 DFS序]

    2434: [Noi2011]阿狸的打字机 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 2545  Solved: 1419[Submit][Sta ...