前言
在JavaScript这门语言当中的数据有很多类型,其中就有数字,字符串,对象等等。我们可以将其大致分为两类:原始类型和引用类型,而在我们以后的编程中可能会遇到判断类型的情况,下面我们一起来看看js中到底如何判断类型的。
1.数据的类型
原始类型
在这种类型中就包含了官方所定义的七种原始类型:
- boolean
- number
- bigint
- symbol
- undefined
- null
- string
上面的这七种类型就构成了我们的原始类型,而它们由于比较简单通常是存储在栈中,当我们想对它进行复制时,通常是直接将值赋予那个变量即可。
引用类型
以下这几种常用的类型我们都将其称为引用类型:
- Array
- Object
- function
上面这几种常见的类型我们将其统称为引用类型,它们由于结构相比于原始类型来说比较复杂,通常是储存在堆中,当我们想对其进行复制时,不是直接赋予字面量,而是通过复制堆中的地址达到复制效果。
2. 类型判断
在每种编程语言中,我们或多或少都会接触到类型判断这一个东西,在JavaScript这门语言当中也不例外,它提供了很多种不同的类型判断方法,下面我们来对几种方法一一剖析并且理解它们的原理。
2.1 typeof
在js中判断类型的方法有很多,首先我们先来了解一下typeof,typeof主要有两种使用方法:
- 当成关键字来使用
- 当成函数来使用
下面我们来看一段代码:
console.log(typeof 123);// typeof当成关键字
console.log(typeof(123));// typeof当成函数
console.log(typeof 'hello');
console.log(typeof true);
console.log(typeof undefined);
console.log(typeof Symbol(1));
console.log(typeof 111n);
我们可以看到上面使用两种方法的效果其实都是一样的,这时候可能就会有同学问了,原始类型还有null
跑哪去了,还有引用类型呢?大家别急,下面我们再来看一段代码:
console.log(typeof null);
console.log(typeof {});// object
console.log(typeof []);// object
console.log(typeof function () {});// function 函数类型会特别判断
当我们看到上面三个object的时候有同学可能会疑惑了,null不是原始类型吗,怎么会判断成object呢?这就不得不谈到typeof的运行机制了。
typeof运行机制:typeof会将需要判断的类型转换为二进制的形式然后对类型进行判断
在typeof判断引用类型时,会将数据转换为二进制的样子。根据js官方规定,所有的引用类型在转换为二进制的时候前三位都是0,而null所有位都是0,这刚好符合了判断object的规则,所以null会被判断成object,而这正是typeof的一个小bug
。
typeof 可以准确的判断除了
null
之外的所有原始类型和函数(判断函数会判断为function
),不能判断引用类型(会全都判断为object
)。
2.2 instanceof
在上文中了解了typeof之后,我们会发现它只能判断原始类型和函数,那么有没有方法可以判断引用类型呢?js官方为了方便我们还打造了一个instanceof用来判断引用类型,下面我跟来看一段代码看看它是如何进行判断的:
console.log({} instanceof Object);
console.log([] instanceof Array);
console.log(new Date() instanceof Date);
console.log(function (){} instanceof Function);
我们可以看到用instanceof可以准确判断引用类型具体是哪个类型,从上面我们可以得到它的使用方法是:
下面我们来思考一下在前文(js原型和原型链)中我们是不是讲过,当进行查找时所有对象当中的隐式原型会不断向上查找,它们最终都会指向Object。那么根据这一特点这些引用类型是不是也会是Object
类型的呢?下面我们来试试:
console.log([] instanceof Object);
console.log(function (){} instanceof Object);
console.log(new Date() instanceof Object);
2.2.1 instanceof的查找方式
我们可以看到确实可以用instanceof来判断引用类型是不是对象,并且能够成功。这时候可能有些同学联想到之前的原型链已经看出了些许端倪,我们先不急,下面再来看一段代码,可以自行想想一下输出是什么:
function Car() {
this.run = 'running'
}
Bus.prototype = new Car()
function Bus() {
this.name = 'byd'
}
let bus = new Bus()
console.log(bus instanceof Bus);
console.log(bus instanceof Car);
console.log(bus instanceof Object);
这时候大家可以看到上面三个输出结果都是true
,第一个输出结果是true这是因为bus是Bus的实例对象,所以bus跟Bus是同一类型,毕竟爸爸跟儿子总得是一个血脉吧。
第二个输出的是true大家可能有点疑惑,我们可以看到上面代码进行了这样一个操作Bus.prototype = new Car()
,这样让Bus的原型变成了Car的实例对象,当我们再用bus和Car进行判断的时候,大家可以联想到之前数组和Object的判断,这里输出的必然也是true。
第三个输出的是true这里大家可能都知道了,因为所有对象都是指向Object的(除了create(null)创建出来的),那么bus instanceof Object是true这就不用解释了。
讲到这里大家可能已经明白了instanceof判断类型的方法就是根据原型链来判断类型相等的,它进行判断的时候会根据隐式原型一步一步向上进行查找直到找到对象,下面我们用上述代码给大家展现一下如何查找的:
console.log(bus instanceof Bus); // bus .__proto__ === Bus.prototype
console.log(bus instanceof Car); // bus.__proto__.__proto__ === Car.prototype
console.log(bus instanceof Object); // bus.__proto__.__proto__.__proto__ === Object.prototype
在这段代码中大家看到了第一个进行判断的时候是判断bus的隐式原型是不是等于Bus的显式原型,第二个是判断Bus.prototype的隐式原始是不是等于Car的显式原型,以此类推得到最后的结果。
在这里大家记instanceof的查找方式可以当成就看前面的是不是后面的子孙,如果是的话就输出true不是的话输出false,这就是根据原型链进行查找。
2.2.2 自定义instanceof
在上文中我们了解了instanceof的查找方式之后,我们可以根据这个查找方式自己定义一个instanceof,下面我们呢来看自己定义的instanceof的代码:
function myinstanceof(l, r) {
while (l.__proto__ !== null) {
l = l.__proto__
if (l === r.prototype) {
return true
}
}
return false
// 递归实现
// if (l === null || typeof l !== 'object') return false
// if (l.__proto__ === r.prototype) return true
// return myinstanceof(l.__proto__, r)
}
console.log(myinstanceof([], Object))
在这里我们可以看到实现了instanceof,那么我们是如何实现的呢?下面我为大家一一讲解一下代码。
在上面代码中,首先我们无法将instanceof写成它原有的使用方法,所以我们将其写为函数形式,第一个参数(l
)传入你想判断的那个变量,第二个参数(r
)传入判断类型。
下面我们根据instanceof的查找方式来编写函数体内部的内容,当我们判断一个变量是不是属于该类型的时候,我们会用对象的隐式原型来进行判断,当l.__proto__ === r.prototype
时,这时候就返回true
。
如果r
有很长一条关系的时候,这时候我们就需要不断地向上查找,这时候就需要一个循环来实现,在每次循环的时候我们都要让l = l.__proto__
从而实现不断向上查找。
那么我们如何终止循环呢?当我们查找到顶部Object
时,它的__proto__
是为null
的这时,我们无法再向下进行查找了,如果还没找到的话,那就再也找不到了就意味着l
不是r
类型的,此时返回false
,我们根据这一特性设置循环终止条件l.__proto__ !== null
。
而下面那个递归的方法同样如此,只是换了一种比较高级点的手法大家可以自行理解一下。
通过上文我们知道了instanceof可以判断引用类型,那么它能不能判断原始类型呢?下面我们来试试:
console.log('hello' instanceof String);
console.log(123 instanceof Number);
console.log(true instanceof Boolean);
我们可以看到它并不能判断原始类型,从而我们可以得出一个结论:
instanceof 通过原型链来判断类型相等,只能判断引用类型(因为原始类型没有隐式原型)
2.2.3 包装类
通过上文我们知道了instanceof不能用来判断原始类型,但是我们前文中说过v8引擎在执行过程中会将原始类型的复制执行为new Number()
类似于这种的样子,有同学就有疑惑了,这不是new
了一个对象出来吗,为什么不能进行判断呢?在这里我们就需要了解一个东西——包装类。
在了解它之前我们先来了解两个个基础知识:
- 原始类型不能拥有属性和方法,属性和方法只能是引用类型的
- 访问对象上不存在的属性会得到 undefined
下面我们来看一段代码:
let num = 123
num.a = 1
console.log(num.a)
//输出:
//undefined
在这里我们有些同学可能会问了,为什么不会报错呢?这不是跟之前原始类型不能拥有属性和方法这个规则冲突了吗?这个之所以没有报错是因为它往包装类身上加了个属性,但是没有赋值。在之前我们说过v8在执行过程中当我们对原始类型进行赋值时,浏览器会给你执行成这样:
而new Number()这种类型我们把它叫做包装类,是v8自动帮我们所包装好的,也可以看成是一个对象。 在了解完这个后我们来解读一下上面代码为什么输出undefined:
我们从num.a = 1
开始,这行代码它是真的会执行,并不会跳过,它真的往num
身上添加了key为a
,值为1
。但是后来v8发现了不对劲,用户要的是字面量,我们怎么能这样呢?我们得满足用户的要求,所以又悄咪咪的把num身上的a给删除了 delete num.a
。
最后我们输出num.a的时候相当于往num上面添加了一个属性a但是我们并没有对其赋值,而我们往对象身上添加属性不进行赋值的时候,输出会是undefined,所以同理,num.a输出的是undefined。
下面我们再来看一段代码:
let n = new Number(123);
n.len = 3
console.log(n.len);
console.log(new Number(123) instanceof Number);// 这个是对象,所以可以用instanceof来判断
console.log(123 instanceof Number);// 这是个原始类型的字面量,并不是对象,而instanceof的原理是根据原型链来判断,所以不能用来判断原始类型
//输出:
//3
//true
//false
在这段代码中我们用户显式的将其定义成了Number类型的对象
,所以v8读取时会将其认为是对象,而不是字面量,所以可以在其身上添加属性。
我们都知道除了数字类型,我们还有字符串类型,而字符串类型身上有很多方法和属性我们可以直接调用,这是为什么呢?
let str = 'hello' // let str = new String('hello')
console.log(str.length);// 读取的是包装类身上的length属性
//输出:
//5
我们可以看到输出了5,这是因为length是包装类身上的一个属性,所以说我们可以进行读取。
2.3 Object.prototype.toString.call(x)
在了解完了typeof和instanceof这两种比较常见的类型判断方法后,下面我们来看看Object身上自带的一种方法 Object.prototype.toString.call(x)
。下面我们来看一段代码:
let b = {}
let a = 1
console.log(Object.prototype.toString.call(a));
console.log(Object.prototype.toString.call(b));
ps:输出的是字符串
通过Object.prototype.toString.call(x)
这个方法我们也可以去判断类型(加call是为了改变toString内部this指向),那么这个方法是如何进行类型判断的呢?这个方法会去读取这个数据结构上的内部属性,而这个内部属性([[Class]]
)就是类型,下面我们来通过浏览器中看一下:
下面我们来看看这个方法的步骤:
- 如果this的值未定义,返回“[object Undefined]”。
- 如果this的值为null,返回“[object Null]”。
- 设 o 为调用 ToObject 的结果,将 this 值作为参数传递ToObject(this)。
- 设 class 为 o 的[[Class]]内部属性,即通过Object.prototype.toString.call(this)获取。([[]]这种语法叫“内部属性”)// 得到了O的类型
- 返回由“[object "+class+"]”三块拼接的结果。
Object.prototype.toString.call(x) 借助Object原型上的toString方法在执行过程中会读取x的内部属性([[Class]])这一机制,所以可以判断出引用类型。
2.4 Array.isArray()
这个方法呢不同于上面方法可以判断很多类型,这个方法只能判断是不是数组,下面我们来看一下如何使用:
let arr = []
console.log(Array.isArray(arr));
console.log(Array.isArray({}));
我们可以看到如果是数组就会输出true否则输出false,而这个方法为什么能实现呢?
其实类似于Object.prototype.toString
这个方法,我们要知道Object.prototype.toString
方法实现的本质是可以看到变量身上的内部属性而Array.isArray()
这个方法同样可以,换句话来说只要可以访问到变量身上的内部属性就都可以来判断类型。
3. 总结
以上四种方法就是类型判断的方法了,下面我们来总结一下各种方法的特点:
typeof 可以准确的判断除了 null 之外的所有原始类型,不能判断引用类型(会全都判断为object,除了function,判断function会判断为function)。
instanceof 通过原型链来判断类型相等,只能判断引用类型(因为原始类型没有隐式原型)
Object.prototype.toString.call(x) 借助Object原型上的toString方法在执行过程中会读取x的内部属性([[Class]])这一机制,所以可以判断出引用类型。
Array.isArray() 判断是否是数组