如何判断 JavaScript 数据类型?

2021/06/08

「如何」系列第四篇,分析了四种判断JavaScript数据类型的方法,总结了1个判断思路。

一、问题

在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:

基本类型 :String、Number、Boolean、Symbol、Undefined、Null

引用类型 :Object

ECMAScript 采用的是松散类型,开发过程中常常会遇到需要检测数据类型的情况,但是JavaScript提供给我们的方法效果各有优劣,本文分析了四种 检测数据类型的方法,并在文章末尾提供了一种组合判断的思路。

二、方法

2.1 typeof

typeof   123   	//"number"
typeof   'abc'  //"string"
typeof    true       //"boolean"
typeof    undefined   //"undefined"
typeof    Symbol('a')   //"symbol"
typeof    null        //"object"
typeof    { }           //"object"
typeof    [ ]           //"object"
typeof    console.log()       //"function"

对于基本类型

  • null 使用返回 "object"

  • 对其他使用返回其字符串 形式的类型名

对于引用类型

  • 对 function 使用返回 "function"

  • 对其他引用数据类型返回 "object"

2.2 instanceof

instanceof 用于判断某个变量是否是某种类型的实例,返回结果是一个布尔值 ,需要两个参数:a incetance of B中,a是待判断的变量, B是作为判断标准的引用类型

let a=[]
a instanceof Array //true
a instanceof Object //true

本质上a instanceof B判断的是 a的原型链上是否存在B.prototype」,但无法判断a直接所属 的类型。所以在上面的例子中,aArray的实例,也是Object的实例

另外对于使用字面量方法定义的基本数据类型,使用 instanceof 来判断它们「是否是某包装类型的实例 」会返回 false,这在技术是是完全正确的 ,但在实际使用中作为基本类型的判断符就不太理想,如下所示:

let b = 1
b instanceof Number //false

let c = "c"
c instanceof String //false

let d = true
d instanceof Boolean //false

instanceof 操作符还有一个问题在于,它假定只有一个全局执行环境 。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
IframeArray = window.frames[0].Array;
var arr =new IframeArray(1,2,3);// [1,2,3]
arr instanceof Array;// false

针对数组的这个问题,ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建。

if(Array.isArray(arr)){
   //对数组执行某些操作
}

Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx]Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array],所以这里直接使用方法四中的Object.prototype.toString.call(arr)也是可以的 。

2.3 constructor

constructor访问的是对象原型 上的cosntructor属性,使用方法如下:

let a=[]
a.constructor //ƒ Array() { [native code] }
a.constructor == Array //true

let b=1
b.contructor == Number //true

let c="str"
c.constructor == String //true

let d=true
d.constructor == Boolean //true

但是这里发生了一件很奇怪的事,不同于incetanceof的判断结果,为什么使用constructor可以正确地判断基本类型?参考《JavaScript 高级程序设计》对基本包装类型 的解释,

在读取模式中访问字符串(或数字、布尔值)时,后台都会自动完成下列处理。
(1) 创建String类型的一个实例;
(2) 在实例上调用指定的方法;
(3) 销毁这个实例。

所以,事实上这里的b.contructor访问的是后台临时 创建的基本包装类型的constructor属性。

这种方法有两个缺陷

  1. null undefined 是没有 constructor 属性的,所以无法通过这种方法判断

  2. 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失constructor 变为重写的prototypeconstructor。(重写prototypeNumber 实例,则此后新建的Parent实例的constructor变为Number,如下所示)

function Parent(){}
let a = new Parent()
a.constructor == Parent //true

Parent.prototype = {prop:"new prototype props"}

let b = new Parent()
b.constructor == Parent //false
b.constructor == Object //true

Parent.prototype = new Number()

let c = new Parent()
c.constructor == Parent //false
c.constructor == Number //true

《JavaScript 高级程序设计》提供了一种解决方案,在重写原型的同时将constructor属性覆盖为需要的值:

function Parent(){}

Parent.prototype = {
    constructor: Parent,
    props:"new prototype props"
}

let a = new Parent()
a.constructor == Parent //true

注意 ,以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true

2.4 toString

toString()Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中Xxx就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call() / apply() 来调用才能返回正确的类型信息。

Object.prototype.toString.call('') ;  // [object String]Object.prototype.toString.call(1) ;   // [object Number]Object.prototype.toString.call(true) ;// [object Boolean]Object.prototype.toString.call(Symbol());//[object Symbol]Object.prototype.toString.call(undefined) ;// [object Undefined]Object.prototype.toString.call(null) ;// [object Null]Object.prototype.toString.call(function Function(){}) ;// [object Function]Object.prototype.toString.call(new Date()) ;// [object Date]Object.prototype.toString.call([]) ;// [object Array]Object.prototype.toString.call(new RegExp()) ;// [object RegExp]Object.prototype.toString.call(new Error()) ;// [object Error]Object.prototype.toString.call(document) ;// [object HTMLDocument]Object.prototype.toString.call(window) ;//[object global] window 是全局对象 global 的引用

这种方法可以准确的判断所有基本类型和原生引用类型 ,但是并不能用于判断自定义类型

function Parent(){}let a = new Parent()Object.prototype.toString.call(a) //[object Object]

三、总结

  1. typeof 不可用于判断null,以及「function以外的引用类型
  2. instanceof 用于判断「后者的原型是否在前者的原型链 上」
  3. constructor 无法用于判断nullundefined,因为他们不具有constructor属性; 重写prototypeconstructor会丢失
  4. toString() 不可用于判断原生引用类型 之外的「自定义类型

可以看到每种方法都有各自的优势和缺陷,针对具体需求的不同,我们可以灵活的使用某一种方法,或者结合多种方法来达成目的。这里提供一种思路:

  1. 第一步,使用toString进行判断,若为基本类型原生引用类型 ,可以直接输出结果

  2. 第二步,对于第一步判断结果为[object Object]自定义引用类型 ,使用constructor进行二次判断