数据类型检测的几种方案!(深挖底层原理)

289 阅读3分钟

数据类型检测

typeof用来检测数据类型的运算符

typeof[value]=>字符串,包含对应的数据类型

  • 局限性:
    • typeof null ->‘object’(null)
    • typeof检测对象类型,除了可执行对象「函数」可以检测出来是“function”,(因为里面有call)其余都是“object”(不能细分对象)
    • 基于typeof检测一个未被声明的变量,结果是“undefined”,而不会报错「基于let在后面声明则会报错」
    • typeof 检测原始值对应的对象类型的值,结果是“object”(不是我们想要的Number)
    • 原理 所有数据类型在计算机底层都是按照二进制的值进行存储的,而typeof就是按照二进制的值进行检测的
      • 性能好
      • 对象的二进制值开头都是“000”,而null的二进制都是零,所以typeof检测null的时候,识别其实是对象(这样是不对的);如果识别是对象,在看对象是否实现了call方法,实现了call方法就返回“function”,没实现的一律返回“object”
    • 应用:
      • 检测除null之外的原始值类型可以使用它

      • 检测是否为对象 if(obj==null||/^(object|function)$/g.test(typeof obj))

      • 检测某个东西是否兼容if(typeof Symbol!=='undefined')

image.png

instanceof 临时用来“拉壮丁”的检测数据类型,本意是检测当前实例是否属于这个类

[value] instanceof Ctor=>true/false

  • 优势细分对象数据类型值「但是不能因为结果是true就说它是标准普通对象」
let arr=[10,20,30,40] //arr.__proto__=>Array.prototype=>Object.prototype
console.log(arr instanceof Array);//true
console.log(arr instanceof Object);//true
  • 弊端:不能检测原始值类型的值「但是原始值对应的对象格式实例则可以检测」
console.log(new Number(1) instanceof Number);//true
console.log(1 instanceof Number);//false
  • 原理:按照原型链检测的;只要当前检测的构造函数(它的原型对象),出现在实例的原型链上,则检测结果就是TRUE;如果Onject.prototype都没有找到结果就是FALSE
function Fn(){}
Fn.prototype=Array.prototype
let f=new Fn//指向数组原型,没有length和索引的哪些东西
console.log(f instanceof Array);//true
  • f instanceof Fn ->FnSymbol.hasInstance
    • 底层如何检测:
    • 当用instanceof的时候首先会检查构造函数Fn有没有Symbol.hasInstance()这个属性,如果有这个属性,FnSymbol.hasInstance是个函数会把实例f传进去,所以f instanceof Fn ===FnSymbol.hasInstance//true
    • Function的原型上会有这个属性:

image.png


function Fn(){}
let f=new Fn;
console.log(f instanceof Fn);//true
console.log(Fn[Symbol.hasInstance](f));//true
console.log(f instanceof Fn===Fn[Symbol.hasInstance](f));//true
  • 用instanceof 的时候会默认这么会调(前提浏览器要有这个属性,一般都有)
  • Fn[Symbol.hasInstance]=function(){}//普通写法的构造函数,它的Symbol.hasInstance属性无法被直接修改;下图中可以看出来Symbol.hasInstance还是函数本身的而不是自己添加的

image.png

  • 但是Es6语法中可以修改,因此也会出现一些问题
class Fn{
    static[Symbol.hasInstance](){
        console.log('OK');
        return true
    }
}
let f=new Fn;
console.log(f instanceof Fn);//true(原因是上边return true,若前面没有return true 则结果是false,由此也可以看出ES6的写法可以修改Symbol.hasInstance属性的)
  • 神奇的事情发生了,如果知道了这个机制,我们则可以改写检测的结果!
class Fn{
    static[Symbol.hasInstance](obj){
        if(Array.isArray(obj))
        return true
        return false
    }
}
let f=new Fn;
let arr=[10,20,30,40]
console.log(arr instanceof Fn);//true
console.log(f instanceof Fn);//false
+ arr instanceof Fn 也会默认调用Symbol.hasInstance这个方法把arr传进来 arr和fn没有关系但是结果是true
  • 总结:底层处理的时候每次用instanceof,底层第一件事情先去调用它的Symbol.hasInstance,如果有则按照这个来,把要检测的实例传递进来,而这个方法它的返回值是啥,它检测的结果就是啥,这个方法如果自己不写则按照Function原型上的方法执行,默认也是按照原型链的查找机制去找,看看当前构造函数的原型有没有出现在实例的原型链上,如果有结果是true 没有结果是false,但是知道了这个机制就可以肆无忌惮的来改了

  • 面试题:重写instanceof

    • 原有的 instanceof 检测左测时实例,右侧必须是个对象,而且是个函数对象(必须有prototype)
    • Object.getPrototypeOf(原始值类型) 默认会有装箱的操作,所以这里就没有继续做判断,也可以自己做
    • 所以箭头函数不行,右边是对象但是不是函数对象也不行
// 我们这个方法支持原始值类型
const instance_of = function instance_of(obj, Ctor) {
    //obj 要检测的实例
    //Ctor 要检测的构造函数
    // 获取obj的原型链一直到Object
    if (Ctor == null)
        throw new TypeError('Right-hand side of instanceof is not an object');
        let type=typeof Ctor
    if (!/^(object)|(function)$/i.test(type))
        throw new TypeError('Right-hand side of instanceof is not an object');
    if (!/^function$/i.test(type))
        throw new TypeError('Right-hand side of instanceof is not callable');
    if (!Ctor.prototype)
        throw new TypeError(
            'Function has non-object prototype undefined in instanceof check'
        );
    let proto = Object.getPrototypeOf(obj);
    while (proto) {
        if (proto === Ctor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }

    return false;
};

console.log(instance_of([], Array)); //true
console.log(instance_of([], RegExp)); //false
console.log(instance_of(1, Number)); //true Object.getPrototypeOf(1)  默认会有一个装箱的操作
console.log(instance_of(Symbol(), Symbol)); //true
console.log(instance_of([], 1)); //报错
console.log(instance_of([], {})); //报错
let f=()=>{}
console.log(instance_of([],f));//报错

constructor 也是拉“壮丁”

  • constructor本身就是存构造函数本身,没有其它意义,但是也可以检测,可以弥补instance of 的一些不足
  • 但是constructor的修改比instanceof更“肆无忌惮”
let arr = [],
    n = 10, //默认有装箱的操作
    m = new Number(10); //

console.log(arr.constructor === Array); //true
console.log(arr.constructor === RegExp); //false
console.log(arr.constructor === Object); //false
// 如果CTOR结果和Object相等,说明当前可能是标准普通对象
console.log(n.constructor === Number); //true
console.log(m.constructor === Number); //true
  • 不能因为结果是false就说它不是对象
function Fn() {}
let f = new Fn();
console.log(f.constructor===Fn);//true
console.log(f.constructor===Object);//false
  • 因为fn的原型重定向会丢失consructor
function Fn() {}
Fn.prototype={}
let f = new Fn();
console.log(f.constructor===Fn);//false
console.log(f.constructor===Object);//true
+ 重定向的原型里没有了constructor,会根据原型链继续查找则会找到Object

image.png

  • 因此不能说它的constructor是Object就说它是纯粹对象,在重定向原型这里它是实例对象

Object.prototype.to String.call([value]) 这是JS中唯一一个检测数据类型没有任何瑕疵的,最准确的,除了性能比typeof 略微少一点,写起来麻烦一点

  • 它的检测结果是固定的格式

image.png

  • "[Object Number/String/Boolen/Null/Undefined/Symbol/BigInt/RegExp/Date/Math/Error...]"
  • 可以解决typeof 检测原始值对象类型值的缺陷
  • 大部分内置类的原型上都有to String方法 一般都是用来转换字符串的,但是Object原型上的是检测数据类型的返回中包含自己所属的构造函数信息

image.png

  • 大部分内置类的原型上都有toString方法,一般都是转换为字符串的但是Object.prototype.to String是检测数据类型的,返回中包含自己所属的构造函数信息...

Object.prototype.to String.call(value)

  • 为啥要用call?
    • ([]).toString() 调用的是Array.prototype上的toString,是转换为字符串 +({}).toString() 调用的是Object.prototype上的toString执行,而且还要改变其中的this等价于->({}).toString.call()
  • 检测返回值遵循啥规则?
    • 一般都是返回当前实例所属的构造函数信息
    • 但是如果实例对象拥有Symbol.toStringTag属性,属性值是啥,最后返回的就是啥,例如:Math[Symbol.toStringTag]="Math"=>Object.prototype.to String.call(Math) =>"[object Math]"
    • Math本身是Object的实例(但是有了Symbol.toStringTag这个属性)

image.png

  • 面试题
  • 如何将检测f的结果改为[object Fn]
class Fn {}
let f = new Fn();
console.log(Object.prototype.toString.call(Fn));//[object Function]
console.log(Object.prototype.toString.call(f));//[object Object]
  • 因为有了Symbol.toStringTag就很轻松解决了这个面试题
class Fn {
   [Symbol.toStringTag]='Fn'
}
let f = new Fn();
console.log(Object.prototype.toString.call(Fn));//[object Function]
console.log(Object.prototype.toString.call(f));//[object Fn]