一、前言
在上一篇博文中 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 操作符。