本文是深入理解JS系列的part5。在实际应用中,我们经常发现typeof和instanceof的表现和我们的预期并不一致,其实这就是因为JS数据类型带来的问题。那么我们从JS的基础概念-数据类型开始,探讨JS中的基本数据类型、基本包装类型、引用类型这三个数据类型的概念和内容,以及对应于typeof、instanceof的不同表现。希望通过这篇文章,我们能够清晰的分辨出变量的类型,并且在合理的应用场景下使用typeof和instanceof。本文在原理部分描述的有些细碎和混乱,大家可以直接看标题中带干货的章节记住结论,或者先看带干货的章节再回头看原理部分,这样理解起来更顺畅:-D
参考书籍:JavaScript高级程序设计-第三版 Nicholas C·Zakas
数据类型概述
JavaScript一共包括6种数据类型:
序号 | 类型 | 值 | 描述 |
---|---|---|---|
1 | Undefined | undefined | 如果一个变量声明但未赋值,那么这个变量就是undefined。当然也可以直接显示初始化变量var a = undefined,但这样并没有什么必要。 |
2 | Null | null | 表示一个空的对象指针,通常用于初始化一个在未来保存object的变量。 |
3 | Boolean | true/false | JS中所有其他类型的值都可以通过转换,变成boolean类型对应的值 |
4 | Number | 整型数值/浮点型数值/NaN/Infinity/-Infinity | JS中所有其他类型的值都可以通过转换,变成Number类型对应的值 |
5 | String | 字符串 | JS中所有其他类型的值都可以通过转换,变成String类型对应的值 |
6 | Object | 集合 | 是一组数据和功能的集合,JS中每个对象最终都是Object类型的实例 |
在JS的世界里,你见到的所有值,最终都是上述六种数据类型之一。
基本数据类型和复杂数据类型
基本数据类型和复杂数据类型是JS数据类型的一种分类方式。
- 基本数据类型(5种):Undefined、Null、Boolean、Number、String,它们是简单数据的表示。
- 复杂数据类型(1种):Object,它是一组数据和功能的集合,同时还包含了其他子类型如function、array等。
我们经常听到一种说法是”JS中万物皆是对象”,这是一种偏哲学的浪漫说法,其实在严格的语言概念上并不是正确的。上面5种基本数据类型本身并不是对象,Object的子类型是对象(这点的详细描述参见下文)。
引用类型和基本包装类型
引用类型的值是引用类型的一个实例,能够通过new操作符创建变量的类型,就是引用类型。比如我们使用new操作符初始化一个对象obj:
这实际上调用了构造函数Object,创建了Object类型的一个新实例,再把实例的地址赋给了变量obj。
通过上面的栗子其实我们知道:
Object是引用类型
除了通过new操作符创建Object对象这种方式,其实还有一种更简单的方式:对象字面量表示法。
这种方法与new方法创建的对象是完全一样的。
String、Number、Boolean也是引用类型
前面我说过,基本数据类型不是对象,一个属于基本数据类型的变量,仅仅是简单的数据表示,而不应该支持任何操作。
然而我们知道,例如String类型的变量,我们可以获取它的长度、复制、获取指定位置的某个字符等等…
这就的因为,String类型也是引用类型,JS对这个类型做了个性化的包装,使得它区别于一般的基本数据类型,具有特殊的行为。Number和Boolean也是如此。
new操作符创建的变量,是对象
引用类型可以通过new操作符显示的创建变量,通过new创建的String、Number、Boolean是对象,它支持substring、contact等操作。
字面量表示法创建的对象,不是对象
当我们用字面量表示法来创建一个属于基本数据类型的变量时,它同样支持各种操作。
这里的原始值”I am a string”是一个不可变的字面量,而不是对象。我们之所以能够对变量msg做各种操作,其实是因为JS引擎自动把字面量转换成了String对象,所以可以访问属性和方法。
那么由字面量表示法创建的变量经由JS引擎转换才成为String对象,这就带来了一点和new方法创建的String对象在表现上的不同点,不同点体现在类型判断上(后面我们会说到)。
简单数据类型中的引用类型就是基本包装类型
也就是是说,String、Number、Boolean就是基本包装类型。它的完整概念就是,JS对这三个简单数据类型做了一些包装,使他们具有其别于其他两种简单数据类型的对应的特殊行为。
数据类型总结(纯干货)
以上就是关于JS数据类型分类的全部内容。前面文字叙述会有些乱,下面我们总结一张表,就一目了然了。
再来一句话描述JS数据类型:
- JS共有6种数据类型,包括Undefined、Null、String、Number、Boolean、Object
- Object类型是复杂数据类型,其余5种是简单数据类型
- 简单数据类型中,String、Number、Boolean是基本包装类型
- 基本包装类型与复杂数据类型是引用类型
再再来一句话描述引用类型的特点:
- 复杂数据类型中,通过new操作符和字面量方法创建的变量都是对象
- 基本包装类型中,通过new操作符创建的变量是对象,通过对象字面量方法创建的变量是字面量,通过JS引擎转换才成为对象
下节将的内容里面,就包括基本包装类型中,通过不同方式方法创建的变量在表现上的一点区别。
判断数据类型
我们知道在JS中,typeof和instanceof用于判断变量的类型。
- typeof判断变量属于哪种数据类型
- instanceof判断变量是那种数据类型的实例
从上面的定义我们就可以猜出这两种方法的应用场景了:typeof用于检测变量的数据类型,instance用于检测引用类型,判断对象属于哪个类型的实例(对象)。
typeof
检测使用字面量表示法创建的变量
考虑以下代码:
我们创建一个变量origin,它的成员包括6种数据类型。然后遍历origin,输出每个成员的键名、值以及typeof检测的类型。控制台输出结果如下
我们发现,使用typeof检测数据类型,除了null检测出奇怪的Object之外,其余5种类型都能被正确检测出来。其实null这个是js设计中的一个bug,不同的变量在底层表示为二进制,js中二进制前三位都为0的话就会被判别我为object类型,而null的二进制表示都是0,因此typeof null === object。
那么我们如何判断一个变量的类型是否为Null呢?我们可以绕过typeof,直接利用全等运算符判断即可。
Object类型包括一些子类型,function、RegExp、Array、Date等(Date只能通过new来创建对象,这里就不列了)。下面我们来检测一下这些变量的类型:
控制台输出结果如下:
我们发现,除了funtion类型是个特例返回function类型外,其它子类型均返回object。
检测使用字new操作符创建的变量
通过前面的讲解我们已经知道,new操作符创建变量的过程就是执行一个构造函数,创建一个实例,这个实例就是一个对象。我们也知道只有引用类型才能使用new操作符创建对象。因此下面的代码只包括引用类型:
控制台输出类型全部是Object。
这就是上一节所提到的,基本包装类型中,通过不同方式方法创建的变量在表现上的一点区别。这个区别就体现在typeof检测数据类型上面。
那么对于Object的子类型,就并没有这个情况。通过字面量表示法和new操作符创建的变量,执行typeof都会返回object,但是typeof function还依然是function。
typeof返回值总结(纯干货)
通过前面的试验,我们又可以总结一张清晰的图:
通过这张图,我们就可以知道为啥前面说对于基本包装类型,字面量表示法创建的对象,不是对象。因为typeof并没有返回object。
instanceof
instanceof用于检测对象属于哪个数据类型的实例。结合前面讲的内容我们可以猜测到,instanceof只适用于引用类型(new操作符生成的实例)。
检测使用字面量表示法创建的变量
考虑以下代码:
控制台输出
相信你已经猜到这个结果了:
- 基本包装类型使用字面量表示法生成的变量,不属于任何类型的实例。
- Object类型使用字面量表示法生成的变量,属于Object类型的实例。
接下来再看看Object的子类型:
控制台输出:
结果说明:
- Object类型的子类型,使用字面量表示法生成的变量,属于他对应子类型的实例,也属于Object类型的实例。
检测使用字new操作符创建的变量
考虑以下代码
控制台输出:
结果说明:
- 基本包装类型使用new操作符生成的变量,属于他对应类型的实例,也属于Object类型的实例。
- Object类型使用new操作符生成的变量,属于Object类型的实例。
那么对于Object的子类型(想必你也猜到了)。new操作符创建的变量,属于他对应子类型的实例,也属于Object类型的实例。
instanceof返回值总结(纯干货)
又是干货时间:
于是这里也可以看出来对于基本包装类型,字面量表示法创建的对象,不是对象*。
而且我们发现,对于new操作符创建的引用类型,它既是自身类型的实例,也是Object类型的实例。这是因为,Object类型是最基础的类型,其他引用类型都是从Object基础上扩展出来的,涉及到原型链和原型继承的理论,这个我们会在深入理解JS系列的后面将。
自己定义构造函数(加餐)
前面我们说过,new操作符的其实就是通过调用构造函数,创建一个实例对象。那么我们来试试DIY。
我们首先定义了一个空函数,然后把地址赋给变量Own。
然后通过new操作符,创造了一个Own的实例对象,并把对象的地址赋给变量entity。
控制台输出结果:
这个栗子可以让我们更好的理解new操作符的原理,以及instanceof更扩展的用途。它不仅可以检测JS基本的类型,还可以检测自定义的对象。至于为啥entity不属于Funtion的实例,这样是涉及到原型链和原型继承的理论,请期待后续文章。
本文汇总干货
一张图说明JS数据类型,以及各类型在不同创建方式下类型检测的不同表现。在实际应用中,如果对typeof和instanceof的使用产生不确定的想法,那么就回来看看这张表吧,相信他可以解决你的问题:-D