JavaScript 获取数据类型

说明

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

概述

前端开发过程中一个常见的功能是:检测某个数据属于什么类型,是字符串、数字、数组、还是对象等等。比如,我们定义了一个函数,并且支持传参,往往就需要对传入的参数进行数据类型检测,然后根据检测结果进行相应的处理,这时我们就必须知道如何准确的获取数据的类型。在构思解决方案之前,我们首先需要回顾一下基础知识,那就是在 JavaScript 中到底有几种数据类型?

数据类型种类

这里所讲的数据类型指的是 JavaScript 语言层面的数据类型,截至目前,共有 8 种类型,可分为【基本数据类型】和【引用数据类型】:

基本数据类型

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

引用数据类型

  • 对象( Object, Array 等等 )

区别

上面提到的【基本数据类型】和【引用数据类型】有什么区别呢?

基本数据类型的值是保存在 "栈" 内存中的,它是可以直接访问的,所有的读写操作都是直接作用于数据本身,中间没有任何 "转接" 行为。

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

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


检测方法

typeof 运算符

这是最简单也是最常用的数据类型检测方法,但同时它也不太 "靠谱",为什么这样说呢?可以先看看下面的代码示例:

js 复制代码
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,例如:

js 复制代码
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()
js 复制代码
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 将无法得到准确的类型:

js 复制代码
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 是基于 "原型" 的,因此它只适用于检测引用数据类型,如:对象、数组等。

我们先来看一下示例:

js 复制代码
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 的原型。

再看一个数组的例子:

js 复制代码
const arr = [ 1, 2, 3 ];
console.log( arr instanceof Array );  // true

同样的原理,arr 是一个通过字面量形式创建的数组,本质上相当于 new Array(),那 arr 的原型链上也必然包含 Array 的原型,因此,上面的逻辑是没问题的,但是如果对代码稍加改造,将 Array 换成 Object 会是什么结果呢?

js 复制代码
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 后返回的结果:

js 复制代码
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 的结果:

js 复制代码
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() 到底能否返回我们想要的结果吧。

js 复制代码
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]

再看看其它类型的数据

js 复制代码
// [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 开头,以 类型] 结尾,那么我们加工一下就可以用它直接返回类型了:

js 复制代码
Object.prototype.toString.call( obj ).slice( 8, -1 );

那这个方法真的绝对保险吗?99% 的情况下是保险的,但不排除极特殊情况,比如:

js 复制代码
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 被改写了,那么这个方法就失效了,不过正常情况下谁会这样做呢?


封装示例

基于以上各种检测手段,我们可以封装一个基本的类型检测方法,下面是一个最基本的封装示例,大家可以自行完善。

js 复制代码
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();
}
相关推荐
web行路人22 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱00124 分钟前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼92142 分钟前
【Ajax】跨域
javascript·ajax·cors·jsonp
超雄代码狂44 分钟前
ajax关于axios库的运用小案例
前端·javascript·ajax
周亚鑫2 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf
落魄小二2 小时前
el-table 表格索引不展示问题
javascript·vue.js·elementui
y5236482 小时前
Javascript监控元素样式变化
开发语言·javascript·ecmascript
fruge2 小时前
纯css制作声波扩散动画、js+css3波纹催眠动画特效、【css3动画】圆波扩散效果、雷达光波效果完整代码
javascript·css·css3
neter.asia2 小时前
vue中如何关闭eslint检测?
前端·javascript·vue.js
嚣张农民2 小时前
JavaScript中Promise分别有哪些函数?
前端·javascript·面试