一、前言
类型判断有时候真的头疼,但是一旦熟练使用就会觉得不过如此。初级的,会判断数字和字符串。中级的,会判断数组和对象。进阶的,会判断日期,正则,错误类型。高级的,会判断 plainObject,空对象,window 对象等等。
基本类型:String、Number、Boolean、Symbol、Undefined、Null
引用类型:Object
基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。
引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆 (heap) 中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function、Array、RegExp、Date 等等。
鉴于 ECMAScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型。对于这个问题,JavaScript 也提供了多种方法,但遗憾的是,不同的方法得到的结果参差不齐。
二、typeof
typeof 是最经常用到的判断类型的。
typeof('saucxs') //'string'
typeof 'saucxs' //'string'
typeof function(){console.log('saucxs')} //'function'
typeof ['saucxs','songEagle',1,2,'a'] //'object'
typeof {name: 'saucxs'} //'object'
typeof 1 //'number'
typeof undefined //'undefined'
typeof null //'object'
typeof /^\d/ //'object'
typeof Symbol // 'function'
其实,typeof 是一个运算符,和加减乘除类似,这就是为啥可以这样写 typeof ‘saucxs’。
在《JavaScript 权威指南》中对 typeof 的介绍:typeof 是一元操作符,放在单个操作数的前面,操作数可以是任意类型。返回值表示操作数的类型的一个字符串。
JavaScript 中一共有 6 中基本数据类型:string,number,boolean,null,undefined,symbol,一种对象类型:object。
分别对应的 typeof 的值,结果不是一一对应的,分别:string,number,boolean,object,undefined,function,对象类型:object。
注意:typeof 可以检测函数类型
但是在 object 下还有很多细分内部属性:Array,Function,Date,RegExp,Error 等。
var date = new Date();
var error = new Error();
console.log(typeof date); // object
console.log(typeof error); // object
所以还需要更好的区分。
三、instanceof
使用 instanceof 的前提条件:object instanceof constructor。object– 要检测的对象。constructor– 某个构造函数。说明使用这个 instanceof 必须是用来检测对象的的类型,不能检测其他类型。
A instanceof B 用来判断 A 是否为 B 的实例。如果 A 是 B 的实例,则返回 true,否则 false。
原理:instanceof 是检测原型。
instanceof (a,B) = {
var l = a.__proto__;
var R = B.prototype;
if(l === R) {
// a 的内部属性 __proto__ 指向 B 的原型对象
return true;
}
return false;
}
分析:a 的_proto_指向 B 的 prototype 时,a 就是 B 的实例。
[] instanceof Array //true
[] instanceof Object //true
new Array([1,43,6]) instanceof Array // true
new Array([1,43,6]) instanceof Object // true
{} instanceof Object // 原型上没有定义 Uncaught SyntaxError: Unexpected token instanceof
({}) instanceof Object; //true
Object.create({'name': 'saucxs'}) instanceof Object //true
Object.create(null) instanceof Object //false 一种创建对象的方法,这种方法创建的对象不是 Object 的一个实例
new Date() instanceof Date //true
new Date() instanceof Object //true
'saucxs' instanceof Object //false
'saucxs' instanceof String //false
new String("saucxs") instanceof Object //true
new String("saucxs") instanceof String //true
1 instanceof Object //false
1 instanceof Number //false
new Number(1) instanceof Object //true
new Number(1) instanceof Number //true
true instanceof Object //false
true instanceof Boolean //false
new Boolean(true) instanceof Object //true
new Boolean(true) instanceof Boolean //true
null instanceof Object //false
undefined instanceof Object //false
Symbol() instanceof Symbol //false
注意:1、new Date 对象既属于 Object,又属于 Date。(他们是由 Object 类派生出来的)。
2、用字面量创建的数组或者构造函数创建的数组,既属于 Object,又属于 Array。
3、用对象字面量创建的对象 object 会报错,{} instanceof Object;使用构造函数创建的对象属于 Object。
4、用字面量的创建的字符串,数字,布尔,既不属于 Object,也不属于各自类型;只有使用构造函数创建的字符串,数字,布尔,既属于 Object,又属于各自的类型。
发现[],构造函数创建的 Date,Object,String,Number,Boolean。既属于自身,又属于 Object。
举个例子,[], Array, Object 的关系:
从 instanceof 能够判断出 [].__proto__ 指向 Array.prototype,而 Array.prototype.__proto__ 又指向了 Object.prototype,最终 Object.prototype.__proto__ 指向了 null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:
从原型链可以看出,[] 的 proto 直接指向 Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是 Object 的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链。因此,instanceof 只能判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。
存在的问题:
它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[0].Array;
var arr = new xArray(1,2,3); // [1,2,3]
arr instanceof Array; // false
针对数组问题,ES5 提供了 Array.isArray() 方法。该方法用以确认某个对象本身是否为 Array 类型。
if (Array.isArray(value)){// 对数组执行某些操作}
Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx],Xxx 就是对应的具体类型。对于数组而言,[[Class]] 的值就是 [object Array]。
四、constructor
定义一个构造函数 Func(),JS 引擎会给 Func 添加 prototype 原型,然后再给 prototype 上添加一个 constructor 属性,并且指向 Func 的引用。
实例化一个函数 func,var func = new Func()。此时 Func 原型上的 constructor 传递到 func 上,因此 func.constructor === Func。
Func 利用原型对象上的 constructor 引用自身,当 Func 作为构造函数来创建实例化对象时,原型上的 constructor 就会遗传到新创建的对象上。从原型链角度讲,构造函数 Func 就是新对象的 func 的类型。这样存在的意义就是新对象产生之后,可以追踪数据类型。
JavaScript 中的内置对象在内部构建时也是这样做的:
'saucxs'.constructor === String //true
new String('saucxs').constructor === String //true
[].constructor === Array //true
new Array([12,56]).constructor === Array //true
new Number(12).constructor === Number //true
new Function(console.log('saucxs')).constructor === Function //true
new Date().constructor === Date //true
new Error().constructor === Error //true
window.constructor === Window //true
document.constructor === HTMLDocument //true
注意:
(1)null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
(2)函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object
为什么变成了 Object?
因为 prototype 被重新赋值的是一个 {},{} 是 new Object() 的字面量,因此 new Object() 会将 Object 原型上的 constructor 传递给 {},也就是 Object 本身。
因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
五、Object.prototype.toString
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]]。这是一个内部属性,其格式为字符串 [object xxx],其中 xxx 就是对象的类型。
这个方法到底是个啥?可以 先看 ES5 规范地址:https://es5.github.io/#x15.2.4.2
toString 方法被调用的时候,会按照这个步骤执行:
(1)如果 this 的值是 undefined,就返回[object Undefined];
(2)如果 this 的值是 null,就返回[object Null];
(3)让 O 成为 ToObject(this)的结果;
(4)让 class 成为 O 的内部属性 [[class]] 的值;
(5)最后返回由 ”[object” 和 class 和 “]” 三个部分组成的字符串。
一句话就是:调用 Object.prototype.toString 会返回一个 ”[object” 和 class 和 “]” 组成的字符串,而 class 要判断对象的内部属性。
console.log(Object.prototype.toString.call(undefined)) // '[object Undefined]'
console.log(Object.prototype.toString.call(null)) // '[object Null]'
console.log(Object.prototype.toString.call(Window)) // '[object Function]'
var date = new Date();
console.log(Object.prototype.toString.call(date)) // '[object Date]'
console.log(Object.prototype.toString.call(Symbol)) // '[object Function]'
注意:通过 call 改变 this 的指向。
所以这个 class 的值是识别对象类型的关键,所以使用 Object.prototype.toString 方法识别出更多的类型,可以识别出至少 11 种类型
var number = 1; // [object Number]
var string = '123'; // [object String]
var boolean = true; // [object Boolean]
var und = undefined; // [object Undefined]
var nul = null; // [object Null]
var obj = {a: 1} // [object Object]
var array = [1, 2, 3]; // [object Array]
var date = new Date(); // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g; // [object RegExp]
var func = function a(){}; // [object Function]
Math //[object Math]
JSON //[object JSON]
注意:
1、其实 Math 对象和 JSON 对象,并不会去判断;
2、Math 对象并不像 Date 和 String 那样对象的类,没有构造函数 Math(), Math.sin()这样的只是函数,不是某一个对象的方法。
六、研究 jquery 的 type API
使用 Object.prototype.toString 这个方法,判断各种类型就比较轻松了,参考了 jquery 的源码的 type 部分:
function type(obj) {
// 一箭双雕
if (obj == null) {return obj + "";}
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj)] || "object" :
typeof obj;
}
其中 class2type 部分
var class2type = {};
// 生成 class2type 映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {class2type["[object" + item + "]"] = item.toLowerCase();})
使用:
type(1); //'number'
type('123456'); //'string'
type(true); //boolean
type(undefined); //undefined
type(null); //'null'
type({name: 'saucxs'}); //'object'
type([1,2,'saucxs',3,4]); //'array'
type(new Date()); // 'date'
type(new Error()); //'error'
type(/^\d/); //'regexp'
type(function(){console.log('saucxs')}); //'function'
type(Symbol); //'function'
这就非常完美的实现了,对我们日常需要类型的判断。
实现了判断日期,正则,错误类型等。
如果还需要判断比较复杂的,比如:空对象,window 对象,类数组对象等等。
七、空对象 EmptyObject
jQuery 提供了 isEmptyObject 方法来判断是否是空对象
function isEmptyObject(obj) {
var name;
for (name in obj) {return false;}
return true;
}
思路:判断空对象就是判断是是否有属性值,for 循环一旦执行,就说明有属性,有属性返回 false。
console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(123)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true
这个判断主要用来区别 {} 和 {name: ‘saucxs’} 就行。
注意点:(1)for in 是 ES6 的属性,这个会遍历原型上的属性。(2)使用 Object.keys(obj)是 ES5 的属性,不会遍历原型上的属性
八、window 对象
window 对象是客户端 js 的全局对象,他有一个 window 属性指向自身。根据这个特性判断是否为 window 对象。
function isWindow(obj){return obj != null && obj ===obj.window;}
注意:一个普通对象拥有 window 属性,并且指向自身。比如这个:
function isWindow(obj) {return obj != null && obj === obj.window;}
let fakeWindow = {}
fakeWindow.window = fakeWindow
isWindow(fakeWindow) // true
是不是可以这么修改呢?
function isWindow(obj) {return !!(window && obj === window)
}
九、类数组对象
如果对类数组没有概念,举个例子:
1、数组和类数组
var arr = [,,3];
对应的类数组是
var arrLike = {
2: 3,
length: 3
}
看 jquery 的源码
function isArrayLike(obj) {
// obj 必须有 length 属性
var length = !!obj && "length" in obj && obj.length;
var typeRes = type(obj);
// 排除掉函数和 Window 对象
if (typeRes === "function" || isWindow(obj)) {return false;}
return typeRes === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
所以如果 isArrayLike 返回 true,至少要满足三个条件之一:
(1)是数组
(2)长度为 0
(3)lengths 属性是大于 0 的数字类型,并且 obj[length – 1]必须存在
第三个条件:数组中用逗号直接跳过的时候,我们认为该元素是不存在的,类数组对象中也就不用写这个元素,但是最后一个元素是一定要写的,要不然 length 的长度就不会是最后一个元素的 key 值加 1。比如数组可以这样写
var arr = [1,,];
console.log(arr.length) // 2
改写成类数组
var arrLike = {
0: 1,
length: 1
}
所以符合条件的类数组对象是一定存在最后一个元素的!
十、判断是不是 dom 元素
isElement 判断是不是 DOM 元素。
isElement = function(obj) {return !!(obj && obj.nodeType === 1);
};
十一、总结
判断类型主要时四个方法:(1)typeof;(2)instanceof;(3)constructor;(4)Object.prototype.toString()。
从基本的六种类型判断,可以使用 typeof,如果涉及到对象的内部类型时候;还可以使用 instanceof,检测对象原型的;还可以使用 constructor 属性是不是指向他们构造函数;需要使用 Object.prototype.toString(),如果需要判断空对象,可以使用 ES6 的 for in 来判断,用 window 属性指向自身来判断是不是 window 对象。以及类数组对象的判断,以及判断是不是 dom 元素的判断。