JavasSript基础之四大常用数据类型检测方法和底层机制

304 阅读6分钟

JS中数据类型检测的办法分类:

  1. typeof [value]
  2. [实例] instanceof [构造函数]
  3. [对象].constructor===[构造函数]
  4. Object.prototype.toString.call([value])

其他小方法:

  • Array.isArray([value]) :检测 [value] 是否是数组
  • isNaN([value]) :检测 [value] 是否为 NaN

typeof

返回一个字符串,字符串中包含了对应的数据类型。 所以 typeof typeof ... 一定是返回 "string"

  • 根据计算机底层存储的“二进制值”来检测的(性能会好一些)
  • 原始值数据类型(不含 null )以及函数值的检测,基于 typeof 处理比较准确

优点:

typeof 10 //'number'
typeof '' //'string'
typeof true //'boolean'
typeof undefined //"undefined"
typeof Symbol('xxx') //"symbol"
typeof 123n//"bigint"

注意:局限性

  • typeof null -> "object"
  • typeof 不能细分对象
  • typeof new Number(10) -> "object" 不能检测将原始值经过包装的数据类型
typeof null//'object'

typeof []//'object'
typeof /^/ 
//正则 'object'
typeof{}//'object'
typeof function(){}//'function'
typeof function*(){}//'function'
typeof Number(10)//'object'

instanceof

[实例] instanceof [构造函数]

本意:不是检测数据类型,而是检测当前实例是否属于这个类(是不是这个类的实例),用来检测数据类型仅是一个延伸的用法,所以存在很多弊端。但是可以基于 instanceof 细分对象类型

console.log([] instanceof Array); //->true
console.log([] instanceof RegExp); //->false
console.log([] instanceof Object); //->true
1 instanceof Number//false
new Number(1)  instanceof Number//true

注意:为什么 [] instanceof Object 也是 true

instanceof 原理:首先按照 [构造函数][Symbol.hasInstance]([实例]) ,如果存在这个属性方法,则方法执行返回的值就是最后检测的结果;如果不存在这个属性方法,则会基于当前 [实例]__proto__ 一直往上找。如果查找中途,找到的某个原型 __proto__ 等于 [构造函数].prototype ,即构造函数的原型出现在其原型链上,则返回结果是 true ,反之 false 。(顺着原型链一直往上找,一直找到 Object.prototype 为止)

注意(局限性):

  • 因为原型可以被重定向,所以检测的结果不一定准确
  • 原始值类型使用 instanceof 是无法检测的
function Fn() {}
Fn.prototype = [];
let f = new Fn;
// 正常理解:f肯定不是数组
console.log(f instanceof Array); //->true
console.log(f instanceof Object); //->true 
console.log((1) instanceof Number); //->false  这样处理不会转换
console.log((1).toFixed(2)); //->'1.00' 这样可以操作是因为浏览器默认会有一个有一个把1转换为对象格式1的操作"Object(1)" 装箱操作

image。png

image。png

所以检测原始值类型也是按照其原理来的

image。png

image。png native code 指的是内置的代码

基于es5代码无法覆盖 [构造函数][Symbol.hasInstance] 方法,但是基于es6是可以重写的,以下代码,我们使用任何值检测 Fn ,都是 true

class Fn {
    // 基于ES6设置静态私有属性是有效的
    static[Symbol.hasInstance](value) {
        console.log(value);
        return true;
    }
}
console.log([] instanceof Fn);//true

image。png

重写 instanceof

需要考虑的边界问题

  1. 传入的构造函数不能是 nullundefined
  2. 因为有一段逻辑需要使用原型判断,所以传入的构造函数必须要有 prototype 。并不是所有的函数都有原型,箭头函数就没有原型,对象也没有原型
  3. 必须是函数

image。png

function instance_of(obj, Ctor) {
    // 数据格式准确性校验//传入的构造函数不能是 ` undefined ` 或者 ` null ` 
    if (Ctor == null) throw new TypeError("Right-hand side of 'instanceof' is not an object");
    //构造函数必须要有 ` prototype ` ,并不是所有的函数都有原型,箭头函数就没有原型
    if (!Ctor.prototype) throw new TypeError("Function has non-object prototype 'undefined' in instanceof check");
    //构造函数必须是函数
    if (typeof Ctor !== "function") throw new TypeError("Right-hand side of 'instanceof' is not callable");

    // 原始类型直接忽略
    if (obj == null) return false;
    if (!/^(object|function)$/.test(typeof obj)) return false;

    // 先检测是否有 Symbol.hasInstance 这个属性
    if (typeof Ctor[Symbol.hasInstance] === "function") return Ctor[Symbol.hasInstance](obj);

    // 最后才会按照原型链进行处理
    let prototype = Object.getPrototypeOf(obj);//获取原型
    while (prototype) {//直到没有原型为止
        if (prototype === Ctor.prototype) return true;//直到找到位置
        prototype = Object.getPrototypeOf(prototype);//继续往上获取
    }
    return false;
}
instance_of([12, 23], Array) //=>true 
instance_of(1, Number) //=>false
instance_of(null, Number) //=>false
instance_of(new Number(1), Number) //=>true

instance_of([], Object) //=>true
instance_of([], RegExp) //=>false

image.png

总结:

  • 优点:可以基于 instanceof 细分对象类型
  • 局限性:
    • 无法检测原始值类型
    • 不准确,因为原型可以被重定向

[对象].constructor===[构造函数]

本意:获取对象的构造函数,用来检测数据类型仅是一个延伸的用法,所以存在弊端, constructor 是可以允许被肆意更改的,检测结果是不准确的

注意区别 instanceof 。这种方法并没有依据原型链继续往上寻找,并且它可以处理原始值类型(装箱操作)(除了 nullundefined 这两个原始值,这两个原始值不允许成员访问)

 let value = [];
console.log(value.constructor === Array); //->true
console.log(value.constructor === RegExp); //->false
console.log(value.constructor === Object); //->false

value = 1;
console.log(value.constructor === Number); //->true

Object.prototype.toString.call([value])

推荐方案:除了写起来麻烦一些,基本没有弊端

以下来源于MDN:

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。

默认情况下, toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖, toString() 返回 "[object type]",其中 type 是对象的类型。

MDN-使用 toString() 检测对象类型

原理:首先找到 Object.prototype.toString 方法,把 toString 执行的之后,让方法中的 this 变为要检测的这个值, toString 内部会返回对应 this (也就是要检测这个值)的数据类型信息 "[object ?]"

补充:

  • 大部分值(实例)所属类的原型上都有 toString 方法。除了 Object.prototype.toStringObject 原型上的这个最原始的 toString )是用来检测数据类型的,其余的一般都是用来转换为字符串的,因为他们在继承时都被重写了。

    image。png

    非普通对象.toString :调取的都是自己所属类原型上的 toString ,并不是 Object.prototype.toString ,基本都是转换为字符串

    普通对象.toString :调取 Object.prototype.toString 这个方法,所以是检测数据类型

     console.log(({}).toString()); //->"[object Object]"
    console.log(Object.prototype.toString.call({})); //->"[object Object]"
    
  • "[object ?]"? 是什么,内部原理是:首先看 [value][Symbol.toStringTag] 属性 ,如果存在这个属性,属性值是什么 ? 就是什么。如果没有这个属性,一般是返回所属的构造函数信息。

    let fn = function* () {};
    console.log(Object.prototype.toString.call(fn)); //->"[object GeneratorFunction]"
    fn[Symbol.toStringTag] //"GeneratorFunction"
    

image。png

  • 返回值包括 "[object Number/String/Boolean/Null/Undefined/Symbol/BigInt/Object/Array/RegExp/Date/Error/Function/GeneratorFunction/Math...]"
    const class2type = {},
        toString = class2type.toString; //->Object.prototype.toString
    console.log(toString.call(1))//[object Number]
    console.log(toString.call(Infinity))//[object Number]
    console.log(toString.call(''))//[object String]
    console.log(toString.call(true))//[object Boolean]
    console.log(toString.call(null))//[object Null]
    console.log(toString.call(undefined))//[object Undefined]
    console.log(toString.call(Symbol()))//[object Symbol]
    console.log(toString.call(10n))//[object BigInt]
    
    console.log(toString.call({}))//[object Object]
    
    console.log(toString.call([]))//[object Array]
    console.log(toString.call(/^/))// [object RegExp]
    console.log(toString.call(new Date))//[object Date]
    
    console.log(toString.call(function(){}))//[object Function]
    console.log(toString.call(()=>{}))//[object Function]
    console.log(toString.call(function* () {}))//[object GeneratorFunction]
    
    console.log(toString.call(Math))//[object Math]
    
    
    注意 Math 是一个对象,里面有一些方法可以使用, Date 是构造函数

面试题:

function Fn() {
    this.x = 100;
}
Fn.prototype = {
    constructor: Fn,
    getX() {
        console.log(x);
    }
};
let f = new Fn;
console.log(Object.prototype.toString.call(f)); //  默认是“[object Object]”

在获取 f[Symbol.toStringTag] 时,一直找到最后都没这个属性,所以浏览器默认是 'Object'

那么如何返回 "[object Fn]"

依据原理去解决即可

function Fn() {
    this.x = 100;
}
Fn.prototype = {
    constructor: Fn,
    getX() {
        console.log(x);
    },
    // 这样所有Fn的实例(f)就拥有这个属性,后期基于toString检测类型,返回“[object Fn]”
    [Symbol.toStringTag]: 'Fn'
};
let f = new Fn;
console.log(Object.prototype.toString.call(f)); // "[object Fn]"