一、前言

类型判断有时候真的头疼,但是一旦熟练使用就会觉得不过如此。初级的,会判断数字和字符串。中级的,会判断数组和对象。进阶的,会判断日期,正则,错误类型。高级的,会判断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 就在内部形成了一条原型链:

image

从原型链可以看出,[] 的 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的引用。

image

实例化一个函数func,var func = new Func()。此时Func原型上的constructor传递到func上,因此func.constructor === Func。

image

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

image

为什么变成了 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元素的判断。


松宝写代码
515 声望47 粉丝

昵称:saucxs | songEagle | 松宝写代码