javascript 数据类型 -- 检测
一、前言
在上一篇博文中 Javascript 数据类型 -- 分类 中,我们梳理了 javascript 的基本类型和引用类型,并提到了一些冷知识。大概的知识框架如下:
这篇博文就讲一下在写代码的过程中,通常怎么检测这些类型。
二、检测
总的来说,我们有4种可检测数据类型的方法, typeof 运算符、 constructor 属性、 instanceof 运算符、 prototype.isPrototypeOf 方法、 Object.prototype.toString.call 方法、 in 操作符、 hasOwnProperty 方法及 isNaN() 、 Array.isArray() 等几种特殊检测方式。每种方法各有优劣,实际运用时还要结合着使用。
typeof 运算符
在上一篇博文中,我们已经用到了 typeof 运算符来检测类别,这里就简单得总结一下它的结果和结论。
typeof '123' // string typeof String('123') // string typeif new String('123') // string typeof 123 // number typeof Number(23.4) // number typeof new Number(2.3e4) // number typeof NaN // number typeof Infinity // number typeof false // boolean typeof Boolean(false) // boolean typeof new Boolean(false) // boolean var mySymbol = Symbol() typeof mySymbol // symbol var a; typeof a // undefined,定义但未初始化 typeof b // undefined,未定义 var div = document.getElementById('abc') console.log(div) // null typeof div // object var Fn = function () {} var classC = class C {} typeof Fn // function typeof classC // function,类在本质上还是函数 typeof {} // object typeof [] // object typeof /\w+/ // object typeof new Map() // object typeof new Set() // object (function () { return typeof arguments })() // object
如上所示, typeof 运算符的语法是 typeof variable 。因是运算符的原由,所以不推荐使用加括号的写法,即( typeof (variable) ) ,以免与方法混淆。运算后的返回值有:
-
- "boolean" :布尔型;
- "string" :字符型;
- "number" :数值型(整数/浮点数值/NaN);
- "symbol" :Symbol 型
- "undefined" :变量未定义,或未初始化;
- "object" :引用型或null;
- "function" :函数型;
由上可知, typeof 运算符对于大部分基本类型的判断还是准确的,特别是 string 、 number 、 boolean 和 symbol ,对于大部分的引用类似会返回 object ,函数虽然也属于引用类型,但由于其具有很对有别于一般引用类型的特性,所以单独判断为 "function" 。若是针对 NaN 进行检测,可以使用全局方法 isNaN(variable) 更为精准。由此,我们可以编写一个统一的方法,先将 undefined 和 null 类型检测出来,再检测其他基本类型。
function is(value, type) { if (value == null) { return value === type } else if (typeof value === 'number' && type === 'isNaN') { return isNaN(value) } return typeof value === type } var x; is(x, undefined) // true var div = document.getElementById('div') is(div, null) // true var y = 2 + undefined is(y, 'isNaN') // true is(3, 'number') // true is('3', 'string') // true is(true, 'boolean') // true is(Symbol(), 'symbol') // true is(function(){}, 'function') // true is(new String('s'), 'object') // true is(new Map(), 'object') // true is({}, 'object') // true
constructor 属性
由于 typeof 运算符并不能准确地检测出引用类型的值,所以我们可以试试 constructor 属性。
当一个函数 foo 被定义是,javascript 会给 foo 函数添加 prototype 原型,然后再在 prototype 上添加一个 contructor 属性,并让其指向 foo 的引用,如下所示:
当执行 var bar = new foo() 时, foo 被当成了构造函数, bar 是 foo 的实例对象,此时 foo 原型上的 constructor 传递到了 bar 上,因此 bar.constructor == foo (返回的是一个true)
。
可以看出,JavaScript在函数 foo 的原型上定义了 constructor ,当 foo 被当作构造函数用来创建对象时,创建的新对象就被标记为 foo 类型,使得新对象有名有姓,可以追溯。所以 constructor 属性可以用来检测自定义类型。同理,JavaScript中的数据类型也遵守这个规则:
'3'.constructor === String // true,由于包装对象的缘故,而拥有了 construtor 属性 String('').constructor === String // true new String('').constructor === String // true (3).constructor === Number // true,与 string 同理 Number(3).constructor === Number // true new Number(3).constructor === Number // true false.constructor === Boolean // true,与 string 同理 Boolean(false).constructor === Boolean // true new Boolean().constructor === Boolean // true Symbol().constructor === Symbol // true ({}).constructor == Object // true [].constructor === Array // true (function(){}).constructor === Function // true new Date().constructor === Date // true new Error().constructor === Error // true new RegExp().constructor === RegExp // true new Map().constructor === Map // true new Set().constructor === Set // true document.constructor === HTMLDocument // true window.constructor === Window // true null.constructor === Object // Uncaught TypeError: Cannot read property 'constructor' of null undefined.constructor === Object // Uncaught TypeError: Cannot read property 'constructor' of undefined
由上可见, constructor 属性对于大部分值都是能准确得出其类型的,特别是几个基本类型,也得益于包装对象的缘故,可以被准确地检测出来。只有 undefined 和 null 由于没有包装对象,不能进行转换,所以不能被检出。因此,上面的判断函数可以改进为:
function is(value, type) { if (value == null) { return value === type // } else if (value.constructor === 'number' && type === 'isNaN') { } else if (value.constructor === Number && type === 'isNaN') { return isNaN(value) } // return typeof value === type return value.constructor === type } var x; is(x, undefined) // true var div = document.getElementById('div') is(div, null) // true var y = 2 + undefined is(y, 'isNaN') // true is(3, Number) // true is('3', String) // true is(true, Boolean) // true is(Symbol(), Symbol) // true is({}, Object) // true is([], Array) // true is(new Map(), Map) // true is(new Set(), Set) // true is(new Date(), Date) // true is(new Error(), Error) // true is(new RegExp(), RegExp) // true is(function(){}, Function) // true is(document, HTMLDocument) // true is(window, Window) // true
当然,根据 construtor 方法的原理可以知道,在判断自定义类型时,由于 prototype 是可以被认为修改的,原有的 construtor 也就会被指向新 prototype 的构造器上。因此,为了规范,在重新定义原型时,一定要给 constructor 重新赋值,以保证构造器不被改变。除非是有预期的希望它改变。
function Animal(){} var cat = new Animal() is(cat, Animal) // true Animal.prototype = {} var dog = new Animal() is(dog, Animal) // false is(dog, Object) // true Animal.prototype.constructor = Animal var tiger = new Animal() is(tiger, Animal) // true is(tiger, Object) // false
instanceof 运算符
在上一节中我们说到,如果手动更改构造函数的 prototpye 的话, constructor 方法就会失效。这种情况下,除了手动为构造函数指定 constructor 外,还有一种方法就是使用 instanceof 运算符。
cat instanceof Animal // false dog instanceof Animal // true tiger instanceof Animal // true
在上面的代码中, cat instanceof Animal 输出的是 false ,为什么呢?
这是因为 instanceof 检测是是对象的原型链中是否包含某个构造函数的 prototype 属性。即 instanceof 检测的是原型。在上面的代码中, Animal 在 cat 实例化之后,将 prototype 属性改成 {},所以 cat._proto_ 中,已经不包括 Animal.prototype 了,所以输出为 false 。所以 instanceof 运算符也存在自定义类型的继承问题。但对于没被修改过 prototype 属性的内置对象而言, instanceof 方法还是可以判断的。
({}) instanceof Object; // true [] instanceof Array; // true (function(){}) instanceof Function; // true /\w+/ instanceof RegExp; // true new Date instanceof Date; // true new Error instanceof Error; // true new Map instanceof Map; // true new Set instanceof Set; // true // 由于默认情况下,对象都是继承自 Object,所以引用类型都是 Object 的实例。 [] instanceof Object // true
由于 instanceof 是根据原型链来进行检查的,所以适用于任何引用类型。而基础类型并没有原型,所以并不能检测出来。对于有包装对象的几个继承类型, instanceof 也不会隐性转换,除非用包装对象进行显性转换才可检测出来。
3 instanceof Number; // false Number(3) instanceof Number // false new Number(3) instanceof Number // true '3' instanceof String; // false String('3') instanceof String // false new String('3') instanceof String // true true instanceof Boolean; // false Boolean(true) instanceof Boolean // false new Boolean(true) instanceof Boolean // true Symbol() instanceof Symbol; // false Object(Symbol()) instanceof Symbol // true // 特别的,虽然 typeof null 等于 object,但 null 并不是 object 的实例 null instanceof Object; // false
prototype.isPrototypeOf() 方法
方法用于测试一个对象是否存在于另一个对象的原型链上,用法为 XXX.prototype.isPrototypeOf(instance) 。
Object.prototype.isPrototypeOf({}); // true Array.prototype.isPrototypeOf([]); ; // true Function.prototype.isPrototypeOf(function(){}); // true RegExp.prototype.isPrototypeOf(/\w+/); // true Date.prototype.isPrototypeOf(new Date); // true Error.prototype.isPrototypeOf(new Error); // true Map.prototype.isPrototypeOf(new Map); // true Set.prototype.isPrototypeOf(new Set); // true // 由于默认情况下,对象都是继承自 Object,所以引用类型都是 Object 的实例。 Object.prototype.isPrototypeOf(function(){}) // true Number.prototype.isPrototypeOf(3); // false Number.prototype.isPrototypeOf(Number(3)); // false Number.prototype.isPrototypeOf(new Number(3)); // true String.prototype.isPrototypeOf('3'); // false String.prototype.isPrototypeOf(String('3')); // false String.prototype.isPrototypeOf(new String('3')); // true Boolean.prototype.isPrototypeOf(true); // false Boolean.prototype.isPrototypeOf(Boolean(true); // false Boolean.prototype.isPrototypeOf(new Boolean(true)); // true Symbol.prototype.isPrototypeOf(Symbol()); // false Symbol.prototype.isPrototypeOf(Object(Symbol())); // true // 特别的,虽然 typeof null 等于 object,Object 并不在 null 的原型链上 Object.prototype.isPrototypeOf(null); // false
由上可知, prototype.isPrototypeOf() 方法与 instanceof 操作符走的是互相逆向的两条路, instanceof 是从实例出发,查找实例上是否有某构造函数的 prototype 属性,而 prototype.isPrototypeOf() 则是从构造函数出发,寻找该构造函数是否在某个已存在的实例的原型链上。
故而,如果 A instanceof B 为真,则 B.prototype.isPrototypeOf(A) 也一定为真。
Object.prototype.toString.call() 方法
在最新的ES6规范中,关于 Object.prototype.toString() 方法是这么规定的:
也就是说,如果上下文对象为 null 和 undefined ,返回 "[object Null]" 和 "[object Undefined]" ,如果是其他值,先将其转为对象,然后依次检测数组、字符串、arguments对象、函数及其它对象,得到一个内建的类型标记,最后拼接成 "[object Type]" 这样的字符串。由此可见, Object.prototype.toString.call() 方法是根正苗红的用来检测内置对象类型的方法。
var _toString = Object.prototype.toString; function is(value, typeString) { // 获取到类型字符串 var stripped = _toString.call(value).replace(/^\[object\s|\]$/g, ''); if (stripped === 'Number' && typeString === 'isNaN') { return isNaN(value); } return stripped === typeString; } var x; is(x, 'Undefined') // true var div = document.getElementById('div') is(div, 'Null') // true var y = 2 + undefined is(y, 'isNaN') // true is(3, 'Number') // true is('3', 'String') // true is(true, 'Boolean') // true is(Symbol(), 'Symbol') // true is({}, 'Object') // true is([], 'Array') // true is(new Map(), 'Map') // true is(new Set(), 'Set') // true is(new Date(), 'Date') // true is(new Error(), 'Error') // true is(new RegExp(), 'RegExp') // true is(function(){}, 'Function') // true is(document, 'HTMLDocument') // true is(window, 'Window') // true
但是由于定义的原因, Object.prototype.toString.call() 对于自定义类型就心有余而力不足了。所以,对于自定义类型就只能借助上面所提到的 contructor 属性或者 instanceof 运算符来检测了。
特殊方式
isNaN():如上所述,专门用来判断 NaN 的数据,用法是 isNaN(variable) ;
Array.isArray():用于确定传递的值是不是 Array。
// 当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes。 var iframe = document.createElement('iframe'); document.body.appendChild(iframe); xArray = window.frames[window.frames.length-1].Array; var arr = new xArray(1,2,3); // [1,2,3] // Correctly checking for Array Array.isArray(arr); // true // Considered harmful, because doesn't work though iframes arr instanceof Array; // false //在兼容性方面,Array.isArray在IE9-浏览器上没有这个方法,所以我们可以改写下方法以兼容低版本浏览器 if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } // or var isArray = function(arr){ return typeof Array.isArray === "function" ? Array.isArray(arr) : Object.prototype.toString.call(arr) === "[object Array]"; }; var arr = [1, 2, 3, 4]; console.log(isArray(arr)); // true console.log(Array.isArray(arr)); // true
三、总结及知识结构图
由此,简单总结下:
-
- Object.prototype.toString.call() :覆盖范围广,特别是对于JS内置数据类型,无论基本类似还是引用类型都适用。缺点是不能检测自定义类型,需要配合用到 instanceof 方法。
- constructor :通过原型链的继承实现检测,对大部分基本类似和引用类型适用,特别是改进后的 is 函数。但也是原型继承的原因,对于自定义类型的检测不太稳定,可配合 instanceof 方法使用。
- instanceof :因为是通过原型链检测的,所以仅适用于引用类型的检测。
- prototype.isPrototypeOf() : instanceof 操作符的逆向操作,结论与 instanceof 操作符相同,同样仅适用于引用类型的检测。
- typeof :适用于基本类型的检测,对于 null 的检测结果是 object ,对于函数的检测结果是 function 。
- isNaN :检测传入值是不是 NaN ,也是该类数据的唯一精准检测方法。
- Array.isArray :检测传入值是不是 Array ,在跨 iframe 时,使用优先级高于 instanceof 操作符。