萝三画室

深入理解JS-part5-数据类型和typeof、instanceof

本文是深入理解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等。

part5_1.png

我们经常听到一种说法是”JS中万物皆是对象”,这是一种偏哲学的浪漫说法,其实在严格的语言概念上并不是正确的。上面5种基本数据类型本身并不是对象,Object的子类型是对象(这点的详细描述参见下文)。

引用类型和基本包装类型

引用类型的值是引用类型的一个实例,能够通过new操作符创建变量的类型,就是引用类型。比如我们使用new操作符初始化一个对象obj:

1
2
3
var obj = new Object();
obj.name = "LoliSuri";
obj.age = 18;

这实际上调用了构造函数Object,创建了Object类型的一个新实例,再把实例的地址赋给了变量obj。

通过上面的栗子其实我们知道:

Object是引用类型

除了通过new操作符创建Object对象这种方式,其实还有一种更简单的方式:对象字面量表示法。

1
2
3
4
var obj = {
name: "LoliSuri",
age: 18
}

这种方法与new方法创建的对象是完全一样的。

String、Number、Boolean也是引用类型

前面我说过,基本数据类型不是对象,一个属于基本数据类型的变量,仅仅是简单的数据表示,而不应该支持任何操作。
然而我们知道,例如String类型的变量,我们可以获取它的长度、复制、获取指定位置的某个字符等等…
这就的因为,String类型也是引用类型,JS对这个类型做了个性化的包装,使得它区别于一般的基本数据类型,具有特殊的行为。Number和Boolean也是如此。

new操作符创建的变量,是对象

引用类型可以通过new操作符显示的创建变量,通过new创建的String、Number、Boolean是对象,它支持substring、contact等操作。

1
2
var msg = new String("I am a string");
var sub = mag.substring(2);//am a string

字面量表示法创建的对象,不是对象

当我们用字面量表示法来创建一个属于基本数据类型的变量时,它同样支持各种操作。

1
2
var msg = "I am a string";
var sub = mag.substring(2);//am a string

这里的原始值”I am a string”是一个不可变的字面量,而不是对象。我们之所以能够对变量msg做各种操作,其实是因为JS引擎自动把字面量转换成了String对象,所以可以访问属性和方法。

那么由字面量表示法创建的变量经由JS引擎转换才成为String对象,这就带来了一点和new方法创建的String对象在表现上的不同点,不同点体现在类型判断上(后面我们会说到)。

简单数据类型中的引用类型就是基本包装类型

也就是是说,String、Number、Boolean就是基本包装类型。它的完整概念就是,JS对这三个简单数据类型做了一些包装,使他们具有其别于其他两种简单数据类型的对应的特殊行为。

数据类型总结(纯干货)

以上就是关于JS数据类型分类的全部内容。前面文字叙述会有些乱,下面我们总结一张表,就一目了然了。

part5_2.png

再来一句话描述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

检测使用字面量表示法创建的变量

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var origin = {
undef: undefined,
nul: null,
bool: true,
num: 1,
str: "haha",
obj:{}
};
(() => {
for (let key in origin){
let value = origin[key];
console.log(key, value, typeof value);
}
})();

我们创建一个变量origin,它的成员包括6种数据类型。然后遍历origin,输出每个成员的键名、值以及typeof检测的类型。控制台输出结果如下

1
2
3
4
5
6
undef undefined undefined
nul null object
bool true boolean
num 1 number
str haha string
obj Object {} object

我们发现,使用typeof检测数据类型,除了null检测出奇怪的Object之外,其余5种类型都能被正确检测出来。其实null这个是js设计中的一个bug,不同的变量在底层表示为二进制,js中二进制前三位都为0的话就会被判别我为object类型,而null的二进制表示都是0,因此typeof null === object。

那么我们如何判断一个变量的类型是否为Null呢?我们可以绕过typeof,直接利用全等运算符判断即可。

1
2
var a = null;
a === null;//true

Object类型包括一些子类型,function、RegExp、Array、Date等(Date只能通过new来创建对象,这里就不列了)。下面我们来检测一下这些变量的类型:

1
2
3
4
5
6
7
8
9
10
11
12
var origin = {
func: function (){},
arr: [],
reg: /\d/,
};
(() => {
for (let key in origin){
let value = origin[key];
console.log(key, value, typeof value);
}
})();

控制台输出结果如下:

1
2
3
func function (){} function
arr [] object
reg /\d/ object

我们发现,除了funtion类型是个特例返回function类型外,其它子类型均返回object。

检测使用字new操作符创建的变量

通过前面的讲解我们已经知道,new操作符创建变量的过程就是执行一个构造函数,创建一个实例,这个实例就是一个对象。我们也知道只有引用类型才能使用new操作符创建对象。因此下面的代码只包括引用类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
var origin = {
bool: new Boolean(true),
num: new Number(1),
str: new String("haha"),
obj: new Object()
};
(() => {
for (let key in origin){
let value = origin[key];
console.log(key, value, typeof value);
}
})();

控制台输出类型全部是Object
这就是上一节所提到的,基本包装类型中,通过不同方式方法创建的变量在表现上的一点区别。这个区别就体现在typeof检测数据类型上面。

那么对于Object的子类型,就并没有这个情况。通过字面量表示法和new操作符创建的变量,执行typeof都会返回object,但是typeof function还依然是function。

typeof返回值总结(纯干货)

通过前面的试验,我们又可以总结一张清晰的图:

part5_2.png

通过这张图,我们就可以知道为啥前面说对于基本包装类型,字面量表示法创建的对象,不是对象。因为typeof并没有返回object。

instanceof

instanceof用于检测对象属于哪个数据类型的实例。结合前面讲的内容我们可以猜测到,instanceof只适用于引用类型(new操作符生成的实例)。

检测使用字面量表示法创建的变量

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var origin = {
bool: true,
num: 1,
str: "haha",
obj:{}
};
(() => {
for (let key in origin){
let value = origin[key];
console.log(key, value, value instanceof Boolean,value instanceof Number,value instanceof String,value instanceof Object);
}
})();

控制台输出

1
2
3
4
bool true false false false false
num 1 false false false false
str haha false false false false
obj Object {} false false false true

相信你已经猜到这个结果了:

  • 基本包装类型使用字面量表示法生成的变量,不属于任何类型的实例。
  • Object类型使用字面量表示法生成的变量,属于Object类型的实例。

接下来再看看Object的子类型:

1
2
3
4
5
6
7
8
9
10
11
var origin = {
func: function (){},
arr: [],
reg: /\d/,
};
(() => {
for (let key in origin){
let value = origin[key];
console.log(key, value, value instanceof Function,value instanceof Array,value instanceof RegExp,value instanceof Object);
}
})();

控制台输出:

1
2
3
func function (){} true false false true
arr [] false true false true
/\d/ false false true true

结果说明:

  • Object类型的子类型,使用字面量表示法生成的变量,属于他对应子类型的实例,也属于Object类型的实例。

检测使用字new操作符创建的变量

考虑以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
var origin = {
bool: new Boolean(true),
num: new Number(1),
str: new String("haha"),
obj: new Object()
};
(() => {
for (let key in origin){
let value = origin[key];
console.log(key, value, value instanceof Boolean,value instanceof Number,value instanceof String,value instanceof Object);
}
})();

控制台输出:

1
2
3
4
bool Boolean {[[PrimitiveValue]]: true} true false false true
num Number {[[PrimitiveValue]]: 1} false true false true
str String {0: "h", 1: "a", 2: "h", 3: "a", length: 4, [[PrimitiveValue]]: "haha"} false false true true
obj Object {} false false false true

结果说明:

  • 基本包装类型使用new操作符生成的变量,属于他对应类型的实例,也属于Object类型的实例。
  • Object类型使用new操作符生成的变量,属于Object类型的实例。

那么对于Object的子类型(想必你也猜到了)。new操作符创建的变量,属于他对应子类型的实例,也属于Object类型的实例。

instanceof返回值总结(纯干货)

又是干货时间:

part5_4.png

于是这里也可以看出来对于基本包装类型,字面量表示法创建的对象,不是对象*
而且我们发现,对于new操作符创建的引用类型,它既是自身类型的实例,也是Object类型的实例。这是因为,Object类型是最基础的类型,其他引用类型都是从Object基础上扩展出来的,涉及到原型链和原型继承的理论,这个我们会在深入理解JS系列的后面将。

自己定义构造函数(加餐)

前面我们说过,new操作符的其实就是通过调用构造函数,创建一个实例对象。那么我们来试试DIY。

1
2
3
4
var Own = function(){};
console.log(Own instanceof Own,Own instanceof Function,Own instanceof Object);
var entity = new Own();
console.log(entity instanceof Own,entity instanceof Function,entity instanceof Object);

我们首先定义了一个空函数,然后把地址赋给变量Own。
然后通过new操作符,创造了一个Own的实例对象,并把对象的地址赋给变量entity。
控制台输出结果:

1
2
false true true
true false true

这个栗子可以让我们更好的理解new操作符的原理,以及instanceof更扩展的用途。它不仅可以检测JS基本的类型,还可以检测自定义的对象。至于为啥entity不属于Funtion的实例,这样是涉及到原型链和原型继承的理论,请期待后续文章。

本文汇总干货

一张图说明JS数据类型,以及各类型在不同创建方式下类型检测的不同表现。在实际应用中,如果对typeof和instanceof的使用产生不确定的想法,那么就回来看看这张表吧,相信他可以解决你的问题:-D

part5_5.png