JavaScrit的变量:如何检测变量类型
在《变量值的数据类型》一文中,了解到了JavaScript的变量主要有基本类型(undefined
、null
、boolean
、number
和string
, ES6中还新增了Symbol
)和引用类型(对象、数组、函数)。但在JavaScript中用户定义的类型(object
)并没有类的声明,因此继承关系只能通过构造函数和原型链接来检查。而在这篇文章中,主要整理了在JavaScript中如何检测一个变量的类型。
在JavaScript中常见的类型检查手段主要有:typeof
、instanceof
、constructor
和toString
几种。接下来主要看看这几种类型检查手段的使用与区别之处。
typeof
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
只能检测基本数据类型,对于null
还有一个Bug。
然而在实际写代码的时候,使用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
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
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
最简单的数据类型检测方法应当算是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
。
总之,如果你要判断的是基本数据类型或JavaScript内置对象,使用toString
; 如果要判断的是自定义类型,请使用instanceof
。
其实,为了便于使用,可以在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 |
- 其他方法包括Duck Testing(假设基于特殊的类型),具体的方法比如有
Array.isArray
、Number.isNaN
和类似于Object.prototype.isPrototypeOf
这样的比较方法。当然可能还有一些我忘记了的方法。 - ES6新增了一种数据类型
symbol
。同时规范是提供了类似Number.isInteger
方法来检测。 ({}).toString.call({})
这是一个缩写版本。在一些浏览器中可以通过window.toString.call
或者toString.call
调用,但它们的结果可能会有所不同。- 每个
window/frame
都有其独立的内置对象。有关于这方面的详细介绍可以阅读这篇文章。 - DOM Element的类型检测可以参见这篇文章。
最后有关于JavaScript中数据类型检查的方法,我们总结为一句口诀:如果你要判断的是基本数据类型或JavaScript内置对象,使用toString
; 如果要判断的时自定义类型,请使用instanceof
。
参考文档
- 如何检查JavaScript变量类型?
- Checking Types in Javascript
- Understanding JavaScript types and reliable type checking
- Fixing the JavaScript typeof operator
- Comparing Type Checks in JavaScript
如需转载,烦请注明出处:https://www.fedev.cn/javascript/comparing-type-checks-in-JavaScript.htmljordan retro 11 mens style