在《变量值的数据类型》一文中,了解到了JavaScript的变量主要有基本类型( undefined
、 null
、 boolean
、 number
和 string
, ES6中还新增了 Symbol
)和引用类型(对象、数组、函数)。但在JavaScript中 用户定义的类型( object
)并没有类的声明,因此 继承关系 只能通过 构造函数和原型链接 来检查 。而在这篇文章中,主要整理了在JavaScript中如何检测一个变量的类型。
在JavaScript中常见的类型检查手段主要有: typeof
、 instanceof
、 constructor
和 toString
几种。接下来主要看看这几种类型检查手段的使用与区别之处。
typeof
操作符返回的是类型字符串,它的返回值有以下几种取值:
类型 | 结构 |
---|---|
Undefined | "undefined" |
布尔值 | "boolean" |
数值 | "number" |
字符串 | "string" |
Symbol | "symbol" |
Null | "object" |
宿主对象(JS环境提供的,比如浏览器) | Implementation-dependent |
函数对象 | "function" |
任何其他对象 | "object" |
来看看代码:
typeof 37; // => "number" typeof "w3cplus"; // => "string" typeof true; // => "boolean" typeof Symbol(); // => "symbol" typeof undefined; // => "undefined" typeof {}; // => "object" typeof function (){};// => "function"
Chrome浏览器运行结果如下图所示:
前面的表格中有一项也显示了,在JavaScript中,使用 typeof
做类型检测,其返回的都说是一个 object
,比如:
typeof ["w3cplus","大漠"]; typeof new Date(); typeof new String("w3cplus"); typeof new function (){}; typeof /test/i;
上面的代码在Chrome浏览器的调试工具中返回的都是 object
:
另外对于 Null
, typeof
检测返回的值也是一个 object
:
typeof null; // => "object"
据说这是 typeof
的一个知名Bug。先忽略其是不是 typeof
的bug,在JavaScript中, null
也是基本数据类型之一,它的类型显然是 Null
。其实这也反映了 null
的语义,它是一个空指针表示对象为空,而 undefined
才表示什么都没有。
根据上面的内容,简单的对 typeof
做一个归纳:
然而在实际写代码的时候,使用 typeof
时需要养成一个好的习惯。比如,使用 typeof
一个较好的习惯是 写一个多种状态的函数 :
function f (x) { if (typeof x == "function") { ... // 当x是一个函数时,做些什么... } else { ... // 其它状态 } }
这样使用较为合理,但不建议像下面这样使用 typeof
:
// 检测是否存在全局变量jQuery if (typeof(jQuery) !== 'undefined') ...
话峰再回转一下,前面使用 typeof
对一个数组做检测的时候也返回 object
。特别是下面的代码,更易让人迷惑:
typeof new Boolean(true) === 'object'; typeof new Number(1) ==== 'object'; typeof new String("abc") === 'object';
一般情况都不建议这样使用。那么在JavaScrit中,可以通过创建一个函数,并且通过一些正则表达式,让这个函数实现一个改进版本的 typeof
。如下所示:
function toType (obj) { return ({}).toString.call(obj).match(//s([a-zA-Z]+)/)[1].toLowerCase(); }
来做一个测试:
toType({name: "大漠"}); // => "object" toType(["W3cplus","大漠"]); // => "array" (function() {console.log(toType(arguments))})(); // => arguments toType(new ReferenceError); // => "error" toType(new Date); // => "date" toType(/a-z/); // => "regexp" toType(Math); // => "math" toType(JSON); // => "json" toType(new Number(4)); // => "number" toType(new String("abc")); // => "string" toType(new Boolean(true)); // => "boolean" toType(function foo() {console.log("Test")}); // =>"function"
将上面的 toType
函数换回成 typeof
,在Chrome重新跑一回,得到的效果将完全不同:
typeof {name: "大漠"}; // => "object" typeof ["W3cplus","大漠"]; // => "object" (function() {console.log(typeof arguments)})(); // => object typeof new ReferenceError; // => "object" typeof new Date; // => "object" typeof /a-z/; // => "object" typeof Math; // => "object" typeof JSON; // => "object" typeof new Number(4); // => "object" typeof new String("abc"); // => "object" typeof new Boolean(true); // => "object" typeof function foo() {console.log("Test")}; // => "function"
instanceof
操作符用于 检测某个对象的原型链是否包含某个构造函数的 prototype
属性 。例如:
// 定义构造函数 function C(){} function D(){} var o = new C(); // true,因为 Object.getPrototypeOf(o) === C.prototype o instanceof C; // false,因为 D.prototype不在o的原型链上 o instanceof D;
o
对象的原型链上有很多对象(成为隐式原型),比如 o.__proto__
, o.__proto__.__proto__
等等。因为 Object.getPrototypeOf(o) === C.prototype
所以返回的是 true
,而 D.prototype
不在 o
的原型链上,所以返回的是 false
。
需要注意的是,如果表达式 o instanceof C
返回 true
,则并不意味着该表达式会永远返回 ture
,因为 C.prototype
属性的值有可能会改变,改变之后的值很有可能不存在于 o
的原型链上,这时原表达式的值就会成为 false
。另外一种情况下,原表达式的值也会改变,就是改变对象 o
的原型链的情况,虽然在目前的ES规范中,我们只能读取对象的原型而不能改变它,但借助于非标准的 __proto__
魔法属性,是可以实现的。比如执行 o.__proto__ = {}
之后, o instanceof C
就会返回 false
了。
instanceof
是通过原型链来检查类型的。所谓的“类型”是一个构造函数。例如:
// 比如直接原型关系 function Animal () {}; var a = new Animal(); a instanceof Animal; // => true // 原型链上的间接原型 function Cat() {}; Cat.prototype = new Animal; var b = new Cat(); b instanceof Animal; // =>
instanceof
除了适用于任何 object
的类型检查之外,也可以用来检测内置兑现,比如: Array
、 RegExp
、 Object
、 Function
:
[1, 2, 3] instanceof Array // true /abc/ instanceof RegExp // true ({}) instanceof Object // true (function(){}) instanceof Function // true
instanceof
对基本数据类型检测不起作用,主要是因为 基本数据类型没有原型链 。
3 instanceof Number // false true instanceof Boolean // false 'abc' instanceof String // false null instanceof XXX // always false undefined instanceof XXX // always false
但我们可以这样使用:
new Number(3) instanceof Number // true new Boolean(true) instanceof Boolean // true new String('abc') instanceof String // true
不过这个时候,都知道数据类型了,再使用 instanceof
来做检测就毫无意义了。
简单总结一下:
instanceof
适用于检测对象,它是基于原型链运作的。
constructor
属性返回一个指向创建了该对象原型的函数引用。需要注意的是, 该属性的值是那个函数本身。 如:
function Animal () {}; var a = new Animal; a.constructor === Animal; // => true
constructor
不适合用来判断变量类型。首先因为它是一个属性,所以非常容易伪造:
var a = new Animal; a.constructor === Array; a.constructor === Animal; // => false
另外 constructor
指向的是最初创建当前对象的函数,是原型链最上层的那个方法:
function Cat () { }; Cat.prototype = new Animal; function BadCat () { }; BadCat.prototype = new Cat; var a = new BadCat; a.constructor === Animal; // => true Animal.constructor === Function; // => true
与 instanceof
类似, constructor
只能用于检测对象,对基本数据类型无能为力。而且因为 constructor
是对象属性,在基本数据类型上调用会抛出 TypeError
异常:
null.constructor; // => TypeError undefined.constructor; // => TypeError
和 instanceof
不同的是,在访问基本数据类型的属性时,JavaScript会自动调用其构造函数来生成一个对象,如:
(3).constructor === Number // true true.constructor === Boolean // true 'abc'.constructor === String // true // 相当于 (new Number(3)).constructor === Number (new Boolean(true)).constructor === Boolean (new String('abc')).constructor === String
另外,使用 constructor
有两个问题。第一个问题它不会走原型链:
function Animal () {}; function Cat () {}; Cat.prototype = new Animal; Cat.prototype.constructor = Cat; var felix = new Cat; felix.constructor === Cat; // => true felix.constructor === Animal; // => false
第二个问题,就是 null
和 undefined
使用 constructor
会报异常。
同样对 constructor
做个简单的总结:
constructor
指向的是最初创建者,而且易于伪造,不适合做类型判断。
JavaScript是运行在宿主环境下的,而每个宿主环境都会提供一套标准的内置对象,以及宿主对象(如 window
, document
),一个新的窗口即是一个新的宿主环境。不同的窗口下的内置对象是不同的实例,拥有不同的内存地址。
而 instanceof
和 constructor
都是通过比较两个 Function
是否相等来进行类型判断的。 此时显然会出问题,例如:
var iframe = document.createElement('iframe'); var iWindow = iframe.contentWindow; document.body.appendChild(iframe); iWindow.Array === Array // false // 相当于 iWindow.Array === window.Array // false
因此 iWindow
中的数组 arr
原型链上是没有 window.Array
的。请看:
iWindow.document.write('<script> var arr = [1, 2]</script>'); iWindow.arr instanceof Array // false iWindow.arr instanceof iWindow.Array // true
最简单的数据类型检测方法应当算是 toString
,不过其看起来像是一个黑魔法:
Object.prototype.toString.call();
toString
属性定义在 Object.prototype
上,因而所有对象都拥有 toString
方法。默认情况之下,调用 {}.toString()
(一个 object
),将会得到 [object object]
。
我们可以通过 .call()
来改变这种情况(因为它将其参数转换为值类型)。例如,通过使用 .call(/test/i)
(正则表达多),这个时候 [object object]
将变成 [object RegExp]
。
Object.prototype.toString.call([]); // => [object Array] Object.prototype.toString.call({}); // => [object Object] Object.prototype.toString.call(''); // => [object String] Object.prototype.toString.call(new Date()); // => [object Date] Object.prototype.toString.call(1); // => [object Number] Object.prototype.toString.call(function () {}); // => [object Function] Object.prototype.toString.call(/test/i); // => [object RegExp] Object.prototype.toString.call(true); // => [object Boolean] Object.prototype.toString.call(null); // => [object Null] Object.prototype.toString.call(); // => [object Undefined]
不过 toString
也不是十全十美的,因为它无法检测用户自定义类型。主要是因为 Object.prototype
是不知道用户会创造什么类型的,它只能检测ECMA标准中的那些内置类型。
function Animal () {}; Object.prototype.toString.call (Animal); // => [object Function] Object.prototype.toString.call (new Animal); // => [object Object]
和 Object.prototype.toString
类似, Function.prototype.toString
也有类似功能,不过它的 this
只能是 Function
,其它类型(如基本数据类型)都会抛出异常。
通过前面的内容介绍,我们可以获知:
typeof
只能检测基本数据类型,对于 null
还有Bug; instanceof
适用于检测对象,它是基于原型链运作的; constructor
指向的是最初创建者,而且容易伪造,不适合做类型判断; toString
适用于ECMA内置JavaScript类型(包括基本数据类型和内置对象)的类型判断; instanceof
和 constructor
。 其实,为了便于使用,可以在 toString
的基础上封闭一个函数。比如 @toddmotto 写的 axis.js :
(function (root, factory) { if (typeof define === 'function' && define.amd) { define([], factory); } else if (typeof exports === 'object') { module.exports = factory(); } else { root.axis = factory(); } }(this, function () { 'use strict'; var axis = {}; var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' '); function type() { return Object.prototype.toString.call(this).slice(8, -1); } for (var i = types.length; i--;) { axis['is' + types[i]] = (function (self) { return function (elem) { return type.call(elem) === self; }; })(types[i]); } return axis; }));
有了这个函数,咱们只需要像下面这样使用,就可以检测数据类型:
axis.isArray([]); // true axis.isObject({}); // true axis.isString(''); // true axis.isDate(new Date()); // true axis.isRegExp(/test/i); // true axis.isFunction(function () {}); // true axis.isBoolean(true); // true axis.isNumber(1); // true axis.isNull(null); // true axis.isUndefined(); // true
每个人都希望有一个完美的解决方案。而事实上,检查数据类型有许多有效的方法,只不过这些方法都各有其利弊,从而印证了那句老话,没有最好的,只有更适合的。同样,对于数据类型检查我们需要针对具体情况和项目需求,采用更为适合的方法。为了能让大家更好的理解,为项目做出最明智的决定,从而写出最好的代码。为了方便起见,下面的表格针对 typeof
、 instanceof
、 constructor
和 toString
做了一个简单的总结:
typeof | instanceof | constructor | toString | |
---|---|---|---|---|
避免字符串比较 | No | Yes | Yes | No |
常用的 | Yes | Yes | No | No |
检查自定义类 | No | Yes | Yes | No |
直接检查null | No | No | No | Yes |
直接检查undefined | Yes | No | No | Yes |
跨窗口工作 | Yes | No | No | Yes |
Array.isArray
、 Number.isNaN
和类似于 Object.prototype.isPrototypeOf
这样的比较方法。当然可能还有一些我忘记了的方法。 symbol
。同时规范是提供了类似 Number.isInteger
方法来检测。 ({}).toString.call({})
这是一个缩写版本。在一些浏览器中可以通过 window.toString.call
或者 toString.call
调用,但它们的结果可能会有所不同。 window/frame
都有其独立的内置对象。有关于这方面的详细介绍可以 阅读这篇文章 。 最后有关于JavaScript中数据类型检查的方法,我们总结为一句口诀: 如果你要判断的是基本数据类型或JavaScript内置对象,使用 toString
; 如果要判断的时自定义类型,请使用 instanceof
。
常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。