JS数据类型检测那些事

2,385 阅读6分钟

背景

总所周知,js是一门动态的弱类型脚本语言,其采用动态的类型系统以及基于原型的继承方式。
缺乏类型的静态约束,这意味着数据类型导致的程序错误并不能在编译阶段及时发现,要想写出健壮的代码,就必须在运行时各种的check&兼容,所以能够熟练准确的检测数据类型成为掌握这门语言最重要的基础之一。

判断数据类型的手段有哪些?

总的来说大致有以下几种:typeof、instanceof、Object.prototype.toString、constructor、鸭式类型、及针对特定类型的检测方法Array.isArray(),Number.isNaN(),虽然方法很多,但他们的使用场景有所不同。

1. 用typeof判断基础数据类型

返回值有undefined、string、number、boolean、object、function、symbol七种。 image.png
可以看出,typeof作为官方提供的类型检测操作符,在检测undefined、string、boolean、symbol这些基本数据类型及function方面是十分靠谱的。表现拉垮的地方主要在于

1) 不能对具体对象类型(ArrayDate、regExp)进行区分。
2typeof null === 'object' // 竟然是true。。。。 

缺陷 2)可以避免,在判断对象引用类型时多判断一句即可,typeof x === 'object' && x !== null。但是不能区分对象的具体类型,确实是个很大痛点。

2. 用instanceof判断对象数据类型

image.png
运算符用于检测某个构造函数的prototype是否出现在目标对象的原型链上。
这是一种预测的检测方式,并不会像typeof一样直接将数据类型以字符串的方式进行返回,而是你需要预判对象类型的构造函数,最终返回一个boolean值。 检测规则其实从命名就可以看出,判断实例是否是由某个构造函数所创建的,那么知道了原理,现在动手实现一个属于自己的instanceof。

function myInstanceof(target,constructor){
  const baseType = ['string', 'number','boolean','undefined','symbol']
    if(baseType.includes(typeof(target))) { return false }
    //原型链其实就是个对象组成的链表,遍历这个链表,
  let prototype = Object.getPrototypeOf(target);
    while(prototype){
        //一旦链上有对象有符合,就返回true
      if(prototype === constructor.prototype){
        return true
      }else{
        prototype = Object.getPrototypeOf(prototype)
      }
    }
    return false
}
console.log(myInstanceof([],Array))

在js里,可以从广义上认为万物源于对象,因为实例虽然是通过构造函数创建的,但是构造函数本身只是没有感情的生产机器,实例的灵魂和性格(公共属性和方法)都是共享自构造函数的prototype属性指向的那个原型对象,而且原型对象都是纯对象,纯对象又是由Object构造函数创建的,那么就会造成下边这种后果。
image.png
对于数组,遍历原型链上的对象,Array.prototype Object.prototype都会出现。 并且,对字面量方式创建的基本数据类型无法进行判断。比如
image.png
如何弥补上边的缺陷呢,答案是可以在上边特殊的场景中采用下边的constructor代替instanceof。

3. 用contructor属性

首先先明确。constructor是原型上的属性,实例继承自原型,所以实例上也能直接访问此属性。 首先看下contructor的通用性表现
image.png
意外的表现不错,除了null、undefined,有contructor属性的基础(包装)类型或者对象类型都能准确判断。

image.png
能准确区分Array|Object 因为它没有instanceof那样会遍历整条原型链,只是在实例身上进行判断。但也有个致命的缺陷,实例上的这一属性太容易被修改了,一旦修改,这个方法就没有意义了。

4. toString方法

首先,js的对象类型或者基础类型的包装对象都有一个toString方法。继承自Object.prototype.toString(),调用会返回对应类型的字符串标记"[object Type]"。 image.png 这个方法有种乱拳打死老师傅,无心插柳柳成荫的感觉,本来的作用只是得到一个表示该对象的字符串,现在用在js类型检测上,表现简直不要太好,针对基础类型及对象类型表现都非常不错,如果非要说个缺点,只能说返回的字符串有点复杂,使用不太方便,现在让我们动手简化一下。
先写一个简版

function isType(type,value){
    return Object.prototype.toString.call(value) === `[object ${type}]`
}
console.log(isType('Array',[]))
console.log(isType('Number',1))

这样使用也不太方便,‘Array’ ‘Number’这样的类型参数,很容易拼写错误,所以希望方法可以预设参数,并且希望构造一个函数工厂,调用返回类似于isArray这样的函数。在IDE中函数名相比字符串会拥有更好的代码提示,不容易拼写错误。

function isType(type){
    return function(value){
        return Object.prototype.toString.call(value) === `[object ${type}]`
    }
}

const isArray = isType('Array')
const isNumber = isType('Number')
console.log(isArray([]),isNumber(1))

这里运用了高阶函数的思想,保留参数+返回一个新的函数,那么可以想到js里bind除了可以绑定this,也有保留参数+返回新函数的功能,用在这里也很合适。

function isType(type,value){
    return Object.prototype.toString.call(value) === `[object ${type}]`
}

const isArray = isType.bind(null,'Array')
const isNumber = isType.bind(null,'Number')
console.log(isArray([]),isNumber(1))

更进一步,用参数柯里化的思想改造一波

function isType(type,value){
    return Object.prototype.toString.call(value) === `[object ${type}]`
}
function curring (fn,...args1){
    let len = fn.length;
    return function(...args2){
        const args = args1.concat(args2);
        if(args.length < len){
            return curring(fn,...args)
        }else{
            return fn(...args)
        }
    }
}
const isArray = curring(isType,'Array')
const isNumber = curring(isType,'Number')
console.log(isArray([]),isNumber(1))

最后,丰富一下支持的类型,大功告成。

const types = [
    'Null',
    'Undefined',
    'String',
    'Number',
    'Boolean',
    'Object',
    'Array',
    'Date',
    'Function',
    'RegExp',
    'Symbol',
    'Math',
]
const checkTypeUtil = {}
types.forEach((type)=>{
    checkTypeUtil[`is${type}`] = curring(isType,type)
})
export {
 checkTypeUtil
}
console.log(checkTypeUtil.isArray([]))

5. 用Array.isArray判断数组

上边提到 instanceof可以用来检测数组,但是这在iframe创建的多window环境中,因为window全局环境需要隔离,所以ArrayArray.prototype在每个窗口中必须是不同的,所以iframeA.Array.prototype ≠ iframeB.Array.prototype,所以 iframeA.arr instanceof iframeB.Array必定是返回false,这是小概率的事件,但是在使用iframe的场景里,互相传值,也是非常可能发生的。使用ES6提供的Array.isArray就没有这个问题,可以准确判断数组。
可以这样 pollify

if (!Array.isArray) {
  Array.isArray = function(x) {
    return Object.prototype.toString.call(x) === '[object Array]';
  };
}

6.区分ArrayLikeArray

类数组的定义是:

  • 拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理
  • 不具有数组所具有的方法
function isLikeArray(x){
    if(!(typeof x === 'object' && x !== null)){
        return false
    }
    return typeof x.length === 'number' && x.length >= 0 && !Array.isArray(x)
}

类数组可以用Array.from Array.prototype.slice.call(val)来转换为真正的数组。

7.判断一个对象是否是纯对象(or普通对象)

纯对象的定义:特指通过一下三种方式创建的对象

  • new Object
  • 对象字面量创建 {}
  • Object.create(null) jquery、lodash源码都是采用下边的方法来检测
const funcToString = Function.prototype.toString
const objectCtorString = funcToString.call(Object)

function isPlainObject(value){
    // 先用toString先排除其他数据类型
    if(!value || !Object.prototype.toString.call(value) === "[object Object]"){
        return false
    }
    const proto = Object.getPrototypeOf(value)
    if(proto === null){//兼容Object.create(null)这样创建的对象
        return true
    }
    const Ctor = Object.prototype.hasOwnProperty.call(proto,'constructor') && proto.constructor;
    if(typeof Ctor !== 'function'){
        return false
    }
    // 这里通过字符串判断构造函数是否是Object,而不是直接使用instanceof,是为了避免上边提到的 多window环境Object不同的问题
    if(funcToString.call(Ctor) === objectCtorString){
        return true
    }
    return false
}
console.log(isPlainObject(Object.create(null)))
console.log(isPlainObject(new Object))
console.log(isPlainObject({a:1}))

8. NaN如何检测,Number.isNaNisNaN有啥区别

image.png
结论:Number.isNaN会严格的判断传入的值是否是直接等于NaN。 isNaN则会先进行Number()转换,然后再进行是否是NaN的判断。

9. 鸭式类型检测法

其实上边利用constuctor判断数据类型,就是采用了这种方法。判断一个动物是不是鸭子,那么通过看起来像鸭子,叫起来像鸭子这样简单的经验判断就可大致进行判断。 比如判断一个对象是不是一个Promise,就可以这样

function isPromise(x){
    if(!(x instanceof Promise)){
        return false
    }
    return typeof x.then === 'function'
}