阐明

本文所介绍的一切知识点、代码示例以及供给的解决方案,均不考虑 IE 浏览器,仅支撑最新版别的 ChromeFirefoxEdgeSafari 浏览器。

概述

前端开发过程中一个常见的功用是:检测某个数据归于什么类型,是字符串、数字、数组、还是目标等等。比如,咱们定义了一个函数,而且支撑传参,往往就需求对传入的参数进行数据类型检测,然后依据检测成果进行相应的处理,这时咱们就有必要知道如何精确的获取数据的类型。在构思解决方案之前,咱们首要需求回忆一下基础知识,那便是在 JavaScript 中到底有几种数据类型?

数据类型品种

这儿所讲的数据类型指的是 JavaScript 言语层面的数据类型,截至现在,共有 8 品种型,可分为【底子数据类型】和【引证数据类型】:

底子数据类型

  • 字符串( String
  • 数字( Number
  • 布尔值 ( Boolean
  • null
  • undefined
  • Symbol
  • BigInt

引证数据类型

  • 目标( Object, Array 等等 )

区别

上面说到的【底子数据类型】和【引证数据类型】有什么区别呢?

底子数据类型的值是保存在 “栈” 内存中的,它是能够直接拜访的,一切的读写操作都是直接效果于数据本身,中心没有任何 “转接” 行为。

引证数据类型的值是保存在 “堆” 内存中的,在 JavaScript 中是不允许直接拜访堆内存中的数据的,要想拜访就需求拿到它在堆内存中的地址,然后经过这个地址进行读写操作。

举个例子:张三要跟李四交流事情,底子数据类型就相当于,张三直接跟李四本人交流。而引证数据类型则相当于张三要跟 “代理人” 交流,再由这个 “代理人” 把张三的需求转述给李四,李四如有反应,也有必要经过 “代理人” 转告给张三,张三和李四由始至终都不能直接交流。


检测办法

typeof 运算符

这是最简单也是最常用的数据类型检测办法,但一起它也不太 “靠谱”,为什么这样说呢?能够先看看下面的代码示例:

console.log( typeof "data" );          // string
console.log( typeof 123456 );          // number
console.log( typeof true );            // boolean
console.log( typeof function () {} );  // function
console.log( typeof Symbol() );        // symbol
console.log( typeof 100n );            // bigint
console.log( typeof undefined );       // undefined
console.log( "===================================" );
console.log( typeof null );            // object
console.log( typeof { a: "a" } );      // object    
console.log( typeof [ 1, 2, 3 ] );     // object

能够看到,关于前七种数据,能检测出相应的类型,而后三种却一概回来 object。前面曾说到,ArrayObject 都归于引证数据类型,而 null 被认为是对空目标的引证,也归归于 Object 范畴,由此可见,typeof 是无法区分出引证数据类型的。

上面的示例中还有一个要害点,那便是 function 函数。函数实践上也是目标,它并不代表一种数据类型,但它却十分特别。函数具有目标的一切才能,但一起它本身还具有特别的属性,而且与目标相比,函数还有一个特别之处,便是它是可调用的,你能够手动调用函数去执行某个操作。根据以上特别情况,在 ECMAScript 标准中规则了能够经过 typeof 区分出函数和其它目标。

除了上述能检测出的七品种型之外,简直其它一切类型经 typeof 检测后都是回来 object,例如:

console.log( typeof document.children );                 // object
console.log( typeof window );                            // object
console.log( typeof document.querySelector( "html" ) );  // object
console.log( typeof document.createElement( "div" ) );   // object
console.log( typeof new Map() );                         // object    
console.log( typeof new Set() );                         // object
console.log( typeof new Promise( () => {} ) );           // object

至此,能够得到一个初步结论,运用 typeof 运算符只能检测出:字符串、数字、布尔值、函数、SymbolBigIntundefined 七品种型,关于数组、目标、null 和其它类型则无能为力,需求另寻他法。

这儿还需求阐明一个特别情况,关于字符串、数字、布尔值这三种底子数据类型,还存在对应的特别引证类型:

  • new String()
  • new Number()
  • new Boolean()
console.log( ( new String( "aa" ) ).valueOf() === "aa" );   // true
console.log( ( new Number( 1234 ) ).valueOf() === 1234 );   // true
console.log( ( new Boolean( true ) ).valueOf() === true );  // true

因而,一旦经过上述的办法创立字符串、数字或许布尔值,运用 typeof 将无法得到精确的类型:

console.log( typeof new String( "aa" ) );   // object
console.log( typeof new Number( 1234 ) );   // object
console.log( typeof new Boolean( true ) );  // object

由此可见,typeof 运算符关于字符串、数字和布尔值的类型判定,无法做到百分百的必定精准。不过,在实践开发中,底子上很少会遇到运用上述特别办法创立这三种数据类型的情况。因而,依然能够继续运用 typeof 进行判别。

instanceof 运算符

以下是 MDN 关于 instanceof 的描绘:

instanceof运算符用于检测结构函数的 prototype 属性是否呈现在某个实例目标的原型链上。

语法:obj instanceof constructor

由于 instanceof 是根据 ”原型“ 的,因而它只适用于检测引证数据类型,如:目标、数组等。

咱们先来看一下示例:

const obj = {
    a: "a"
};
console.log( obj instanceof Object );  // true
console.log( Object.getPrototypeOf( obj ) === Object.prototype );  // true

在上面的示例中,obj 是一个经过字面量办法创立的目标,本质上相当于 new Object(),也便是说,obj 是由 Object() 结构函数构建出来的,那么 obj 的原型链上必定包括 Object 的原型。

再看一个数组的例子:

const arr = [ 1, 2, 3 ];
console.log( arr instanceof Array );  // true

同样的原理,arr 是一个经过字面量办法创立的数组,本质上相当于 new Array(),那 arr 的原型链上也必定包括 Array 的原型,因而,上面的逻辑是没问题的,可是如果对代码稍加改造,将 Array 换成 Object 会是什么成果呢?

const arr = [ 1, 2, 3 ];
console.log( arr instanceof Object );  // true

成果显现也为 true,这是由于在 JavaScript 中,数组其实也是目标,不仅仅是数组,凡是经过 new 要害字创立的实例本质上都是目标。所以,前文说到的 typeof new xxx 的成果都是 object。也正因如此,数组的原型链中也必定包括 Object 的原型。

别的需求阐明的是,instanceof 在多 iframe 环境下会存在问题,由于这意味着存在多个全局环境,而不同的全局环境具有不同的全局目标,然后具有不同的内置类型结构函数,这将会导致 instanceof 呈现紊乱。

Object.prototype.toString.call()

这种绝妙的检测办法最早是由 ”始祖级“ 的 JavaScript 类库 Prototype.js 发掘出来的。这简直要追溯到近 20 年前了,那时的前端还处在萌发时期,各种标准标准没有完善,还要面对令人抓狂的浏览器兼容问题,因而要想精确检测出各种数据类型简直是难如登天。各大程序库想尽了办法,各种奇技淫巧层出不穷,直到这种办法的呈现,总算有了一个安稳的检测办法,之后的库和框架也底子都是用此办法来检测数据类型。

它的底子原理实践上便是输出目标内部的类属性 [[Class]] 的值,这在绝大多数情况下是必定精确的。这儿先看第一个知识点:toString

简单来说,toString 办法便是将目标以字符串的办法回来。JavaScript 中简直一切目标都有 toString 办法,nullundefined 没有 toString 办法,下面经过代码示例看一下每品种型调用 toString 后回来的成果:

console.log( ( new String( "a" ) ).toString() );    // a
console.log( ( new Number( 100 ) ).toString() );    // 100
console.log( ( new Boolean( true ) ).toString() );  // true
console.log( [ 1,2,3 ].toString() );                // 1,2,3
console.log( { a: "a" }.toString() );               // [object Object]
console.log( Symbol().toString() );                 // Symbol()
console.log( 100n.toString() );                     // 100

上述成果能够看出,每个目标的 toString 办法都有自己的一套逻辑, 因而输出的成果不尽相同,而且上面的成果也阐明了,单纯运用各自的 toString 办法得到的值也没能表示出相关类型,只有一个 [object Object] 值得研究。

为什么会得到 [object Object] 呢?这是由于目标的 toString 办法无法将目标正确解析为字符串,所以 JavaScript 引擎直接回来了字符串 [object Object]。此时咱们能够做出一个这样的假设:由于是在 object 类型的数据上调用了 toString 办法,回来了 [object Object],而这个字符串中的两个单词都是 object(先不考虑大小写),能否阐明这个字符串实践现已包括了类型信息呢?如果这个假设建立,那么理论上其它类型的数据应该也能够经过这种办法获取到类型。可是前面说到了,每个目标的 toString 办法都有自己的一套逻辑,回来的内容五花八门,现在就需求想办法让它们也能回来相似 [object Object] 这种办法的字符串,以此来揣度其所属类型。这儿就需求用到原型属性,由于一切的目标都继承自 Object,既然它们各自的 toString 办法有自己的逻辑,那咱们就不必他们本身的 toString,而是运用继承自 Object 原型上的 toString, 也便是 Object.prototype.toString,那为什么后边还用了一个 call 呢? 先来看一下不必 call 的成果:

console.log( Object.prototype.toString( [] ) );    // [object Object] 
console.log( Object.prototype.toString( {} ) );    // [object Object]
console.log( Object.prototype.toString( "aa" ) );  // [object Object]
console.log( Object.prototype.toString( 11 ) );    // [object Object]

单纯运用 Object.prototype.toString 将一概回来 [object Object],由于这始终是在调用 ObjecttoString 办法,其内部的 this 始终指向的是 Object,所以就有必要要借助 call 改动 this 的指向( apply 也能够 ), 所以才有了 Object.prototype.toString.call() 的写法。其实能够这样理解:我自己的 toString 被我重写了,不能用了,那我就用 ObjecttoString,由于它是原始纯净的,能回来我想要的东西,而且我继承自 Object,能借用它的一切,自然也就能借用它的 toString,只需在借用时注明是我在运用就能够了( call 的效果 )。

下面就看看运用 Object.prototype.toString.call() 到底能否回来咱们想要的成果吧。

console.log( Object.prototype.toString.call( "aa" ) );            // [object String]
console.log( Object.prototype.toString.call( 1000 ) );            // [object Number]
console.log( Object.prototype.toString.call( true ) );            // [object Boolean]
console.log( Object.prototype.toString.call( 100n ) );            // [object BigInt]
console.log( Object.prototype.toString.call( null ) );            // [object Null]
console.log( Object.prototype.toString.call( undefined ) );       // [object Undefined]
console.log( Object.prototype.toString.call( Symbol() ) );        // [object Symbol]
console.log( Object.prototype.toString.call( [ 1,2,3 ] ) );       // [object Array]
console.log( Object.prototype.toString.call( { a: "a" } ) );      // [object Object]
console.log( Object.prototype.toString.call( function () {} ) );  // [object Function]

再看看其它类型的数据

// [object Promise]
console.log( Object.prototype.toString.call( new Promise( () => {} ) ) ); 
// [object HTMLHtmlElement]
console.log( Object.prototype.toString.call( document.querySelector( "html" ) ) );
// [object HTMLDivElement]
console.log( Object.prototype.toString.call( document.createElement( "div" ) ) );
// [object HTMLCollection]
console.log( Object.prototype.toString.call( document.children ) );
// [object HTMLDocument]
console.log( Object.prototype.toString.call( document ) );
// [object Window]
console.log( Object.prototype.toString.call( window ) );
// [object Set]
console.log( Object.prototype.toString.call( new Set() ) );
// [object Map]
console.log( Object.prototype.toString.call( new Map() ) );

依据以上成果能够得知,回来成果都是以 [object 最初,以 类型] 结束,那么咱们加工一下就能够用它直接回来类型了:

Object.prototype.toString.call( obj ).slice( 8, -1 );

那这个办法真的必定稳妥吗?99% 的情况下是稳妥的,但不排除极特别情况,比如:

Object.prototype.toString = () => "哈哈哈";
console.log( Object.prototype.toString.call( "aa" ) );  // 哈哈哈
console.log( Object.prototype.toString.call( 1000 ) );  // 哈哈哈
console.log( Object.prototype.toString.call( true ) );  // 哈哈哈
console.log( Object.prototype.toString.call( 100n ) );  // 哈哈哈

由此可见,如果最原始的 Object.prototype.toString 被改写了,那么这个办法就失效了,不过正常情况下谁会这样做呢?


封装示例

根据以上各种检测手法,咱们能够封装一个底子的类型检测办法,下面是一个最底子的封装示例,我们能够自行完善。

function getType ( data ) {
    // 关于简单的类型直接运用 typeof 判别
    let type = "";
    switch ( typeof data ) {
        case "string":    type === "string";    break;
        case "number":    type === "number";    break;
        case "boolean":   type === "boolean";   break;
        case "function":  type === "function";  break;
        case "symbol":    type === "symbol";    break;
        case "bigint":    type === "bigint";    break;
        case "undefined": type === "undefined"; break;
    }
    if ( type ) {
        return type;
    }
    // 数组类型直接运用原生供给的 Array.isArray
    if ( Array.isArray( data ) ) {
        return "array";
    }
    // 其他类型运用 Object.prototype.toString.call 获取
    return Object.prototype.toString.call( data ).slice( 8, -1 ).toLowerCase();
}