由于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. mariadb 最新精简压缩版 win64 解压即用

    包含版本: mariadb-10.1.18-winx64 mariadb-5.5.53-winx64 32的没有压缩,估计用的人也比较少. 网盘链接: http://pan.baidu.com/s/1 ...

  2. 案例借鉴 | 某通讯巨头的IT建设方案

    成都联通作为合并重组后的中国联通在成都的分支机构,拥有基础扎实的通信网络和当前最先进技术的WCDMA网络.随着3G和4G业务的发展领先,成都联通凭借其出色的网络能力和服务,在用户中赢得了口碑. 在IT ...

  3. Atitit 衡量项目的规模

    Atitit 衡量项目的规模 1. 预估衡量项目的规模的方法1 1.1. 方法一.Delphi 法1 1.2. 方法二. 类比法1 1.3. 方法三.功能点估计法1 1.4. 方法四.PERT估计法2 ...

  4. 记录一次Quartz2D学习(四)

    (三)内主要讲了图片与文字的绘制 本次主要讲解 绘制状态的保存与恢复,以及对它的使用 4.绘制状态 4.1 绘制状态的保存与恢复,以及对它的应用 TIP:通过对保存恢复绘制状以及多次的渲染,可以绘制出 ...

  5. Oracle hint

    1.use_concat 网上说法: CONCATENATION和UNION/UNION ALL操作比较类似,根据OR查询条件,将一个查询分解为两个或更多的部分,然后在去掉两个部分重复的记录.由于CO ...

  6. easyui表格的增删改查

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  7. Memcached初探

    一.Memcached是什么 Memcached是由Danga Interactive开发的,高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度. Memcached基于 ...

  8. log4net在Realse下有个好大的坑呀。

    原因:项目在DEBUG编译下日志是好好的,但是生成了Realse布署后却无日志产生了. 查找: 官方指导:http://logging.apache.org/log4net/release/faq.h ...

  9. 【Linux命令】文件和目录操作命令

    本文主要用于常用命令的备忘,具体用法可用man查看,或查询其他资料. cd:改变工作目录 ls:列出目录的内容 mkdir:创建一个目录 cat:连接并显示指定的一个和多个文件的有关信息 cp:将给出 ...

  10. 切换“使用被动式FTP”

    -- 本文版权归博客园和dige1993所有,访问作者博客 -- 最近用Dreamweaver做了几个网页,打算上传到远程FTP服务器的时候,同步文件和连接FTP服务器时总是超时出错,一直处在&quo ...