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();
}
相关推荐
CodeClimb2 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
光头程序员4 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
fmdpenny5 小时前
Vue3初学之商品的增,删,改功能
开发语言·javascript·vue.js
小美的打工日记5 小时前
ES6+新特性,var、let 和 const 的区别
前端·javascript·es6
涔溪5 小时前
有哪些常见的 Vue 错误?
前端·javascript·vue.js
程序猿online6 小时前
前端jquery 实现文本框输入出现自动补全提示功能
前端·javascript·jquery
Turtle8 小时前
SPA路由的实现原理
前端·javascript
HsuYang8 小时前
Vite源码学习(九)——DEV流程中的核心类(下)
前端·javascript·架构
傻小胖8 小时前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
蓝冰凌10 小时前
【整理】js逆向工程
javascript·js逆向