JavaScrit的变量:如何检测变量类型

发布于 大漠

在《变量值的数据类型》一文中,了解到了JavaScript的变量主要有基本类型(undefinednullbooleannumberstring, ES6中还新增了Symbol)和引用类型(对象、数组、函数)。但在JavaScript中用户定义的类型(object)并没有类的声明,因此继承关系只能通过构造函数和原型链接来检查。而在这篇文章中,主要整理了在JavaScript中如何检测一个变量的类型。

在JavaScript中常见的类型检查手段主要有:typeofinstanceofconstructortoString几种。接下来主要看看这几种类型检查手段的使用与区别之处。

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浏览器运行结果如下图所示:

typeof

前面的表格中有一项也显示了,在JavaScript中,使用typeof做类型检测,其返回的都说是一个object,比如:

typeof ["w3cplus","大漠"];
typeof new Date();
typeof new String("w3cplus");
typeof new function (){};
typeof /test/i;

上面的代码在Chrome浏览器的调试工具中返回的都是object

typeof

另外对于Nulltypeof检测返回的值也是一个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"

typeof

将上面的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"

typeof

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的类型检查之外,也可以用来检测内置兑现,比如:ArrayRegExpObjectFunction

[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

第二个问题,就是nullundefined使用constructor会报异常。

同样对constructor做个简单的总结:

constructor指向的是最初创建者,而且易于伪造,不适合做类型判断。

跨窗口问题

JavaScript是运行在宿主环境下的,而每个宿主环境都会提供一套标准的内置对象,以及宿主对象(如window,document),一个新的窗口即是一个新的宿主环境。不同的窗口下的内置对象是不同的实例,拥有不同的内存地址。

instanceofconstructor都是通过比较两个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类型(包括基本数据类型和内置对象)的类型判断;
  • 基于引用判等的类型检查都有跨窗口问题,比如instanceofconstructor

总之,如果你要判断的是基本数据类型或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

总结

每个人都希望有一个完美的解决方案。而事实上,检查数据类型有许多有效的方法,只不过这些方法都各有其利弊,从而印证了那句老话,没有最好的,只有更适合的。同样,对于数据类型检查我们需要针对具体情况和项目需求,采用更为适合的方法。为了能让大家更好的理解,为项目做出最明智的决定,从而写出最好的代码。为了方便起见,下面的表格针对typeofinstanceofconstructortoString做了一个简单的总结:

  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.isArrayNumber.isNaN和类似于Object.prototype.isPrototypeOf这样的比较方法。当然可能还有一些我忘记了的方法。
  • ES6新增了一种数据类型symbol。同时规范是提供了类似Number.isInteger方法来检测。
  • ({}).toString.call({})这是一个缩写版本。在一些浏览器中可以通过window.toString.call或者toString.call调用,但它们的结果可能会有所不同。
  • 每个window/frame都有其独立的内置对象。有关于这方面的详细介绍可以阅读这篇文章
  • DOM Element的类型检测可以参见这篇文章

最后有关于JavaScript中数据类型检查的方法,我们总结为一句口诀:如果你要判断的是基本数据类型或JavaScript内置对象,使用toString; 如果要判断的时自定义类型,请使用instanceof

参考文档

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/comparing-type-checks-in-JavaScript.htmljordan retro 11 mens style