在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:
基本类型:Null,Undefined,String,Number,Boolean,Symbol(ES6)
引用类型:Object
基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。
引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。
鉴于 ECMAScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型,对于这个问题,JavaScript 也提供了多种技术方案,但遗憾的是,不同的方案得到的结果也参差不齐。
下面介绍常用的 4 种方案,并对各个方案存在的问题进行简单的分析。
1. typeof
typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
1 | typeof ''; // string 有效 |
有些时候,typeof 操作符会返回一些令人迷惑但技术上却正确的值:
- 对于基本类型,除 null 以外,均可以返回正确的结果。
- 对于引用类型,除 function 以外,一律返回 object 类型。
- 对于 null ,返回 object 类型。
- 对于 function 返回 function 类型。
其中,null 有属于自己的数据类型 Null ,引用类型中的 数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,其实返回object也没有错,因为所有对象的原型链最终都指向了Object,Object是所有对象的”祖宗”。 但当我们需要知道某个对象的具体类型时,typeof 返回的显然不是我们想要的结果。
2. instanceof
instanceof 是用来判断 A 是否为 B 的实例对,表达式为:A instanceof B,如果A是B的实例,则返回true,否则返回false。 在这里需要特别注意的是:instanceof检测的是原型
,我们用一段伪代码来模拟其内部执行过程:
1 | instanceof (A,B) = { |
从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为A就是B的实例,我们再来看几个例子:
1 | [] instanceof Array; //true |
我们发现,虽然 instanceof 能够判断出 [] 是Array的实例,但它认为 [] 也是Object的实例,为什么呢?
我们来分析一下[]、Array、Object 三者之间的关系:
从instanceof 能够判断出 [].proto 指向 Array.prototype, 而 Array.prototype.proto 又指向了Object.prototype,Object.prototype.proto 指向了null,标志着原型链的结束。因此,[]、Array、Object就形成了如下图所示的一条原型链:
从原型链可以看出,[] 的 proto 直接指向Array.prototype, 间接指向Object.prototype, 所以按照 instanceof 的判断规则,[] 就是Object的实例。当然,类似的new Date()、new Person() 也会形成这样一条原型链,因此,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型
。
instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
为了解决这个问题, ECMAScript 5 新增了 Array.isArray() 方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。这个方法的用法如下。
1 | if (Array.isArray(value)){ |
支持 Array.isArray()方法的浏览器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 10.5+和 Chrome。
3. constructor
当一个函数 F被定义时,JS引擎会为F添加 prototype 原型,然后再在 prototype上添加一个 constructor 属性,并让其指向 F 的引用。如下所示:
当执行 var f = new F() 时,F 被当成了构造函数,f 是F的实例对象,此时 F 原型上的 constructor 传递到了 f 上,因此 f.constructor == F
可以看出,F 利用原型对象上的 constructor 引用了自身,当 F 作为构造函数来创建对象时,原型上的 constructor 就被遗传到了新创建的对象上, 从原型链角度讲,构造函数 F 就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。
同样,JavaScript 中的内置对象在内部构建时也是这样做的:
细节问题:
- null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断
- 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object
为什么变成了 Object?
因为 prototype 被重新赋值的是一个 { }, { } 是 new Object() 的字面量,因此 new Object() 会将 Object 原型上的 constructor 传递给 { },也就是 Object 本身。
因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。
4. Object.prototype.toString
toString是Object原型对象上的一个方法,使用 call 来调用该方法会返回调用者的类型字符串,格式为 [object,xxx],xxx 是调用者的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument等,基本上,所有对象的类型都可以通过这个方法获取到。
1 | Object.prototype.toString.call('') ; // [object String] |
需要注意的是,必须通过 call 或 apply 来调用,而不能直接调用 toString ,从原型链的角度讲,所有对象的原型链最终都指向了 Object , 按照JS变量查找规则,其他对象应该也可以直接访问到 Object 的 toString 方法,而事实上,大部分的对象都实现了自身的 toString 方法,这样就可能会导致 Object 的 toString 被终止查找,因此要用call/apply来强制执行 Object 的 toString 方法。