JavaScript数据类型检测方式及底层运行原理

342 阅读3分钟

JS中数据类型检测的四种方案

  • typeof 检测数据类型的运算符

  • instanceof 检测该实例是否为该类的实例

  • constructor 用于获取实例或类的构造函数

  • Object.prototype.toString.call() 用于Object类原型上检测数据类型的方法

typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型

let num = 10;
console.log(typeof num);	// number
let str = 'lucky';
console.log(typeof str);	// string
let bool = true;
console.log(typeof bool);	// boolean
let und = undefined;
console.log(typeof und);	// undefined
let sym = Symbol('sym');
console.log(typeof sym);	// symbol
let obj = {name:'lucky'};
console.log(typeof obj);	// object
let func = function(){};
console.log(typeof func);	// function

typeof typeof num;	// => 这里不论 num 是什么类型,最终的结果都是: "string" 

typeof 原理

typeof 操作符是按照数据在计算机底层存储的二进制结果来进行检测的

  • nulltypeof null的结果是 "Object"是由于null在底层中存储的二进制是:000000,对象在底层存储的二进制是000xxx;所以使用typeof检测null的时候会打印出"Object"。在之后ECMAScript也曾经提出过修正案,但是由于导致浏览器中出现typeof null === 'null',可能会导致旧项目不兼容的情况,所以最后提案被拒绝了。
  • 对象类型:所有的对象类型的数据在浏览器底层的存储的二进制都是000开头,所以基于typeof检测对象类型的结果都是"Object",这就导致typeof无法细分对象和数组。

instanceof

为了解决typeof无法区分具体对象的缺点,于是就把instanceof这个操作符拉过来用做数据类型检测具体对象数据类型

  • instanceof不是用于检测数据类型的,而是用来检测当前实例是否属于这个类
  • instanceof一般只能用于普通对象数组对象正则对象日期对象等对象的具体细分
  • instanceof不是专业的数据类型检测,所以存在很多的问题

结果为true也不一定是Object的实例

let arr = [];
arr instanceof Array;	// true
arr instanceof Object;	// true
arr instanceof RegExp;	// false

instanceof无法用于检测基本数据类型

let n = 10;
let m = new Number(10);
//能够调用Number原型上的方法,那么它一定是Number类的实例
console.log(n.toFix(2));  //"10.00"  => 基本类型值在调用原型上的方法时,浏览器会将基本类型值Object(n)处理一下,然后他就可以基于原型链查找方法了
console.log(m.toFix(2));  //"10.00" 

n instanceof Number;    //false
m instanceof Number;    //true

手动修改原型链,会影响instanceof的检测结果

function Person(){}
Person.prototype = Array.prototype;

let p1 = new Person;
p1 instanceof Array;	// true

instanceof原理

基于" 实例 instanceof 类" 检查数据类型的时候,浏览器底层会将它处理成 “类.hasInstance(实例)” 的方式。(所有类都是Function的实例)

let arr = [];
arr instanceof Array;               //true
Array[Symbol.hasInstance](arr);     //true

let obj = {};
arr instanceof obj;     //对象不是function的实例,他没有这个属性

Symbol.hasInstance方法执行的原理

  • 判断当前实例的原型链上__proto__是否存在这个类的原型prototype
  • arr.__proto__ === Array.prototype 满足条件 arr instanceof Array返回true
  • arr.__proto__.__proto__ === Object.prototype 同样满足条件 arr instanceof Object返回true

重写instanceof

/*
 *  [parmas1] obj 要检测的实例
 *  [patmas2] constructor 要检查的类
 */
function instance_of(obj,constructor){
    // 参数校验
    if(obj ==  null || !/^(Object|Function)$/i.test(typeof obj)) return false;
    if(typeof constructor !== 'function') throw new TypeError("Right-hand side of 'instanceof' is not callable");
    
     // obj.__proto__ === Object.getPrototypeOf(obj) 这里使用兼容写法
    let proto = Object.getPropertyOf(obj),
        prototype = constructor.prototype;
    while(true){
        if( proto === null ) return false;
        if( proto === prototype ) return true;
        proto = Object.getPropertyOf(proto);
    }
}

constructor

constructor是原型上一个指向构造函数的属性,它同样不是用于专业的数据类型检测的

let arr = [];
console.log(arr.constructor === Array); //true  在constructor不被修改的情况下,这样区分是数组还是普通对象
console.log(arr.constructor === Object); //false
console.log(arr.constructor === RegExp); //false 

原型重定向

function Person() {}
Person.prototype = Array.prototype;
let p1 = new Person;
console.log(p1.constructor === Array); //true 一但原型重定向,constructor也改了,所以也就不准了

检查基本数据类型,相较于instanceof来说对基本数据类型检查的支持更好

let n = 10;
let m = new Number(10);
console.log(n.constructor === Number); //true
console.log(m.constructor === Number); //true

Object.prototype.toString.call()

专业的用于检测数据类型的方法,除了代码长,可以说是零瑕疵

  • 许多类原型上的toString方法都是用于转换字符串的,而Object.prototype.toString是用于检测数据类型的,所以使用时一般搭配call来改变this的指向
  • ``Object.prototype.toString.call()的返回结果是[object Xxxxxx]Xxxxxx的可能是对象[Symbol.toStringTag]对象.constructor或者Object`。
let class2type = {},
    toString = class2type.toString; //Object.prototype.toString
toString.call(10);   			//[object Number]
toString.call('A');  			//[object String]
toString.call(true);  			//[object Boolean]
toString.call(null);  			//[object Null]
toString.call(undefined);  		//[object Undefined]
toString.call(Symbol());  		//[object Symbol]
toString.call(/^$/);  			//[object RegExp]
toString.call(function());  	//[object Function]
toString.call(function* ());  	//[object GeneratorFunction]

constructor修改也不会影响最后的返回值

// 所以可以借用Symbol.toStringTag来修改自定义类的检测结果
class Person {
    // 只要获取实例的[Symbol.toStringTag]属性值,则调用这个方法
    get[Symbol.toStringTag]() {
        return "Person";
    }
}
let p1 = new Person; 
toString.call(p1) => "[object Person]" 

jQuery中的数据类型检测方法

jQuery中封装的数据类型检测的方式或许不是最好的,但是也是提供了一个很好的思路

var class2type = {};
var toString = class2type.toString;

["Boolean","Number","String","Function","Array","Date","RegExp","Object","Error","Symbol"].forEach((type)=>{
    class2type[`object ${type}`] = type.toLowerCase();
})

function toType(obj){
    if(obj == null){
        return obj + '';
    }
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[toString.call(obj)] | "object" :
    	typeof obj;
}