在JS中,我们通常需要判断一个量是什么类型。而JS官方也给我们打造了几种可以进行类型判断的方法。今天,我们就来好好掌握一下它。
1. JS中的数据类型
我们再来复习一下JS中的数据类型。
# 原始类型
string
number
boolean
undefined
null
symbol
bigint
# 引用类型
object
array
function
Date
在JS中数据分为原始类型和引用类型。经过上一篇文章的学习,我们知道了引用类型之所以为引用类型,是因为引用类型的数据全部存放在堆里,在调用栈里存放的仅是它的引用地址。
当面试官问我们JS中一共有多少种数据类型时,我们还是回答:一共有八种。前七种是原始类型,第八种就叫对象。
2. typeof
我们先来聊聊JS的第一种进行类型判断的方法typeof。在它的身上有些什么特点呢。
我们先来完成这样一个简单的场景:写一个函数,返回输入的两个参数的和。这非常容易。
function add(x, y) {
return x + y
}
这个方法是建立在我们传进来的参数一定是Number类型的数据。那如果我们传的是字符串呢?比如‘1’+‘2’。我们先想让它能正确输出3,这时我们就得判断传进来的参数的数据类型了。我们可以用上typeof。
function add(x, y) {
if (typeof x === 'number' && typeof y === 'number') {
return x + y
} else {
return Number(x) + Number(y)
}
}
console.log(add('1', '2'));
我们先判断如果传进来的参数是Number类型,直接返回它们的和。如果不是,就将他们转换为Number类型,再进行相加。
当然这里是小题大做了,我们直接return Number(x) + Number(y)一行代码就行了。这里是展示一下typeof的可能用法。还有个小细节,我们传进来字符串‘1’、‘2’ typeof 能帮我们转换成数字,那要是传一个‘hello’呢?
它得到的是“NaN”类型,它隶属于Number类型,全称为“Not a Number”,表达的是无法表达的数字。
那对于typeof,我们已经知道了它的用法。那它能判断出所有数据的类型吗?我们在上一篇文章中提到过,我们再来回顾一下。
对于原始类型,当我们用它来判断除了null以外的类型时,它都能判断成功。而对于null,它返回的却是对象。
我们在上一篇文章解释了原因。如有遗忘,可以回去巩固。
[>](深入剖析JS的内存机制我们已经学过了JS的执行机制、作用域链和闭包。今天我们来好好聊聊JS的内存机制。当我们写下一段JS - 掘金)
而对于引用类型。它会把引用类型都判断成对象。
除了函数,函数有点特别,typeof能正确判断。
所以对于typeof,我们有个结论:typeof 可以准确的判断除了null之外的所有原始类型,不能判断引用类型(除了function)。
3.instanceof
既然typeof能判断原始类型,那应该也会有个方法能判断引用类型。那就是instanceof。这个方法就有意思了,我们先来认识一下它,再把它的原理搞清楚。
3.1 instanceof的用法与特点
我们来见识一下它的用法。
console.log({} instanceof Object);
instanceof前面放要进行判断的数据类型,后面放数据类型。它和typeof不一样。typeof是如果能读得懂你是什么类型,直接返回你的类型。而instanceof是用来判断你是不是隶属于某一种类型,返回true or false。
对于引用类型,它都能准确的判断。
console.log({} instanceof Object); //true
console.log([] instanceof Array); //true
console.log(new Date() instanceof Date); //true
console.log(function () { } instanceof Function); //true
那对于原始类型,它能否成功判断呢?
console.log('hello' instanceof String);
按理来说,你是判断前面那个是否隶属于后面那个,那字符串'hello'确实是属于String的呀,应该判断成功才对。
结果却是false。对于其它原始类型,结果还是false。
console.log('hello' instanceof String); //false
console.log(123 instanceof Number); //false
console.log(true instanceof Boolean); //false
再来,当我们这样判断:
console.log([] instanceof Object);
我们拿一个数组去判断它是否为一个对象。我们的期望应该是它能够给我们返回一个false,但它返回的却是true。
这是为什么呢?这就不得不聊到instanceof的执行原理了。
3.2 instanceof的原理
我们这样写:
function Bus() { }
let bus = new Bus()
console.log(bus instanceof Bus);
我们定义了一个构造函数Bus,利用new调用构造函数获得一个实例对象bus,我们再来判断bus是不是Bus类型。因为bus就是由Bus产生的,所以bus隶属于Bus没毛病,它应该返回一个true。
确实是true。再来:
function Car() {
this.run = 'running'
}
Bus.prototype = new Car()
function Bus() {
this.name = 'BYD'
}
let bus = new Bus()
console.log(bus instanceof Car);
我们又创建了一个构造函数Car,然后让Bus的原型赋值为Car创建的实例对象。此时,bus能访问到name,因为bus就是new调用Bus创建出来的实例对象。那bus能访问到run吗?也能,因为我们赋值给了Bus的原型,所以run就在Bus的原型上,也就会赋值给bus的隐式原型。
我们去判断bus是否属于Car,结果会是什么呢?
它返回的是true。说明bus隶属于Car。这是为什么呢?明明bus不是由Car创建的。
我们大概已经能猜到instanceof的执行原理了。它应该是按照原型链去判断的对吧。对于console.log(bus instanceof Bus),是不是有bus.__proto__ === Bus.prototype,因为bus是由new调用构造函数Bus创建的,所以new干了这样一个操作,将Bus的显示原型赋值给了bus的隐式原型。
而对于console.log(bus instanceof Car),是不是有bus.__proto__.__proto__ === Car.prototype。因为我们Bus.prototype = new Car(),Bus.prototype本身也是一个对象,而我们用new去调用了Car,new就将Bus.prototype这个对象的隐式原型赋值为了Car的显示原型,所以有Bus.prototype.__proto__=== Car.prototype,而又因为bus.__proto__ === Bus.prototype,所以bus.__proto__.__proto__ === Car.prototype成立。
说明instanceof确实是用原型链去判断的。当左边的隐式原型不等于右边的显示原型时,它就会去找左边的隐式原型的隐式原型,直到找不到为止返回false,否则返回true。
知道了这一点,我们就能自己来写一份instanceof的源码了。
3.3 instanceof的源码
function myinstanceof(L, R) {
}
console.log(myinstanceof([], Array));
我们自己定义一个函数myinstanceof,接收两个形参L和R。我们期望传进去的[]和Array它给我们返回true。
我们已经知道了原理是按原型链去判断的。那我们应该判断传进来的实参,左边的隐式原型是否等于右边的显示原型。如果相等返回true。
function myinstanceof(L, R) {
if (L.__proto__ === R.prototype) {
return true
}
}
console.log(myinstanceof([], Array));
如果不相等,它就会找左边的隐式原型的隐式原型。所以我们得写一个循环。
function myinstanceof(L, R) {
while (L !== null) {
L = L.__proto__
if (L === R.prototype) {
return true
}
}
return false
}
console.log(myinstanceof([], Array));
因为原型链的顶端是Object显示原型的隐式原型,即为null。所以我们将循环条件设为L !== null,一直找直到找不到隐式原型为止。
然后我们将L赋值为L的隐式原型,拿L去和R.prototype做判断。如果相等返回true,如果不等,继续循环,再将L赋值为此时L的隐式原型,L又变成了自己隐式原型的隐式原型。如此循环,直到找不到隐式原型为止,两边都不相等的话,我们就返回false。
我们来运行一下看看结果。
给我们返回了true。这就是instanceof的执行原理。
3.4 包装类
我们再来回到我们在3.1提出的问题。
console.log([] instanceof Object);
这个我们已经知道为什么了。因为数组一直找它的隐式原型一定能找到Object的显示原型。
而对于原始类型:
console.log('hello' instanceof String); //false
console.log(123 instanceof Number); //false
console.log(true instanceof Boolean); //false
这为什么不行呢?我们不是说过,当创建一个字面量时,v8引擎会执行成new去调用构造函数创建一个实例对象出来吗?那按理来说对于这个实例对象,肯定能找到它的隐式原型属于构造函数的显示原型啊。
再来看这个:
console.log(new String('hello') instanceof String); // true
console.log('hello' instanceof String); //false
对于上面那行代码,我们拿new String('hello') 去判断,它能判断成功。下面的却不行。说明关键点在这个new身上吧。
这就得聊到包装类的概念。类就是JS中的对象,我们也可以说成对象的包装。
关于包装类的第一个定理:原始类型不能拥有属性和方法,属性和方法只能是引用类型。
我们来看这样一段代码:
let num = 123;
num.a = 1
console.log(num.a);
请问这段代码合理吗?我们往一个数字身上添加一个属性a,这应该报错才对。
但结果却是undefined。
其实v8引擎在读到这段代码时,它会进行这样一个操作。let num = 123会执行成let num = new Number(123)创建一个实例对象,然后读到num.a = 1这行代码时,确实往实例对象上增加了这个属性。
在读取值的时候V8引擎就会执行第一条定理:原始类型不能拥有属性和方法,属性和方法只能是引用类型。 所以v8引擎转念一想,用户定义的是一个字面量,是一个原始类型,我们就不能让它拥有属性和方法。于是它就delete num.a,将这个属性删除。所以去访问时得不到值。
而当我们去访问对象上不存在的属性时,它不会报错,会输出undefined,所以上面那段代码不报错。
let num = 123; //let num = new Number(123)
num.a = 1 // 真的往 num 上添加了key为a,值为1
// 不对! 因为用户想要的是字面量,必须满足用户要求
// delete num.a
console.log(num.a); // unfefined 不报错
而当我们这样写时:
let num = new Number(123)
num.a = 1
console.log(num.a);
它能输出1。
因为此时我们用new去调用了构造函数创建出来了num,说明用户确实是相把num当做对象去使用,所以能输出成功。
当我们用new去调用一个构造函数时,得到的就是一个包装类。
3.5 instanceof的总结
现在,我们已经知道了instanceof的用法与特点,还有执行原理。它可以判断引用类型,不能判断原始类型。它是按照原型链去判断的。
所以对于instanceof:instanceof 通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)。
4. Object.prototype.toString.call(x)
还有一种方法,它既能判断原始类型又能判断引用类型。它就是Object.prototype.toString.call(x)。我们来看看它是怎么用的。
let a = 1
console.log(Object.prototype.toString.call(a))
它能成功判断a为Number类型。
let b = {}
console.log(Object.prototype.toString.call(b))
它也能成功判断引用类型。
那么它的原理是什么呢?
我们在this那篇文章中详细解释过call的原理。它是用来显示绑定this的。例如a.call(b),它能将a的this指向b。我们说过它的原理是让b去短暂的拥有a,然后去b.a()触发隐式绑定就能将a中的this指向b。
所以对于Object.prototype.toString.call(x),我们就让x短暂拥有了Object原型上的toString方法,也就是执行了x.toString()。那为什么这个方法能返回x的数据类型呢?我们就得聊到toString的执行原理了。
我们可以在JS的官方文档上查到toString的执行原理。
Object.prototype.toString()
- 如果 this 值为 undefined,则返回 “[object Undefined]”。
- 如果 this 值为 null,则返回 “[object Null]”。
- 设 O 为调用 ToObject 的结果,将 this 值作为参数传递。
- 设 class 为 O 的 [[Class]] 内部属性的值。
- 返回 String 值,该值是连接三个字符串 “[object ”、class 和 “]” 的结果。
因为我们让Object.prototype.toString的this指向了x,所以此时的this就为x。
看第一和第二条,当this值为undefined或null时,他能返回‘[object Undefined]’或‘[object Null]’,也就是成功判断了undefined和null的数据类型。
那第三、四、五条是什么意思呢?
第三条,将this值作为参数传递给ToObject,也就是执行了ToObject(this)操作,然后赋值给 O。
第四条,定义一个变量class,赋值为 O 的 [[Class]] 内部属性的值。这个[[Class]]就是JS内定的属性,v8能用我们不能用。它的值就是一个数据的数据类型。
第五条,这样我们就得到了this值的数据类型,存放在变量class中,然后返回一个String值,以“[object ”、class 和 “]”的方式。
这就是Object.prototype.toString() 执行规则。所以Object.prototype.toString.call(x) 也能用来判断x的数据类型。
其实底层原理就是为了读取数据内定的一个属性[[Class]]的值,里面存放着数据的数据类型。
所以对于Object.prototype.toString.call(x),我们有一个结论:Object.prototype.toString.call(x)借助Object原型上的toString方法在执行过程中会读取x的内部属性[[class]]这一机制。
5. Array.isArray(x)
还有最后一种判断类型的方法,就是Array.isArray(x)。它只能用来判断一个值是否为数组类型,返回一个布尔值。
let arr = []
console.log(Array.isArray(arr));
只有数组身上有这个方法。
6. 总结
至此我们已经学完了JS中所有进行内存判断的方法。
-
typeof 可以准确的判断除了null之外的所有原始类型,不能判断引用类型(除了function)
-
instanceof 通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)
-
Object.prototype.toString.call(x)借助Object原型上的toString方法在执行过程中会读取x的内部属性[[class]]这一机制
-
Array.isArray(x)