说明
本文所介绍的所有知识点、代码示例以及提供的解决方案,均不考虑 IE
浏览器,仅支持最新版本的 Chrome
、Firefox
、Edge
和 Safari
浏览器。
概述
前端开发过程中一个常见的功能是:检测某个数据属于什么类型,是字符串、数字、数组、还是对象等等。比如,我们定义了一个函数,并且支持传参,往往就需要对传入的参数进行数据类型检测,然后根据检测结果进行相应的处理,这时我们就必须知道如何准确的获取数据的类型。在构思解决方案之前,我们首先需要回顾一下基础知识,那就是在 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
。前面曾提到,Array
和 Object
都属于引用数据类型,而 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
运算符只能检测出:字符串、数字、布尔值、函数、Symbol
、BigInt
和 undefined
七种类型,对于数组、对象、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
方法,null
和 undefined
没有 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]
,因为这始终是在调用 Object
的 toString
方法,其内部的 this
始终指向的是 Object
,所以就必须要借助 call
改变 this
的指向( apply
也可以 ), 所以才有了 Object.prototype.toString.call()
的写法。其实可以这样理解:我自己的 toString
被我重写了,不能用了,那我就用 Object
的 toString
,因为它是原始纯净的,能返回我想要的东西,并且我继承自 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();
}