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 2;           // => "number"
typeof "qdskill";    // => "string"
typeof true;         // => "boolean"
typeof Symbol();     // => "symbol"
typeof undefined;    // => "undefined"
typeof {};           // => "object"
typeof function (){};// => "function"

Chrome浏览器运行结果如下图所示:

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

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

typeof ["qdskill","Skill"];
typeof new Date();
typeof new String("qdskill");
typeof new function (){};
typeof /test/i;

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

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

另外对于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: "qdskill"}); // => "object"
toType(["qdskill","Skill"]); // => "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"

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

将上面的toType函数换回成typeof,在Chrome重新跑一回,得到的效果将完全不同:

typeof {name: "qdskill"}; // => "object"
typeof ["qdskill","Skill"]; // => "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"

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

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 // truenew 
Boolean(true) instanceof Boolean // truenew 
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类型(包括基本数据类型和内置对象)的类型判断;
  • 基于引用判等的类型检查都有跨窗口问题,比如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

总结

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

typeofinstanceofconstructortoString
避免字符串比较NoYesYesNo
常用的YesYesNoNo
检查自定义类NoYesYesNo
直接检查nullNoNoNoYes
直接检查undefinedYesNoNoYes
跨窗口工作YesNoNoYes
  • 其他方法包括Duck Testing(假设基于特殊的类型),具体的方法比如有Array.isArrayNumber.isNaN和类似于Object.prototype.isPrototypeOf这样的比较方法。当然可能还有一些我忘记了的方法。
  • ES6新增了一种数据类型symbol。同时规范是提供了类似isInteger方法来检测。
  • ({}).toString.call({})这是一个缩写版本。在一些浏览器中可以通过toString.call或者toString.call调用,但它们的结果可能会有所不同。
  • 每个window/frame都有其独立的内置对象。有关于这方面的详细介绍可以阅读这篇文章
  • DOM Element的类型检测可以参见这篇文章

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

原文链接:https://www.w3cplus.com/javascript/comparing-type-checks-in-JavaScript.html

发表评论

登录后才能评论