详解JS中类型判断的方法以及包装类

217 阅读12分钟

在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’呢?

image.png

它得到的是“NaN”类型,它隶属于Number类型,全称为“Not a Number”,表达的是无法表达的数字。

那对于typeof,我们已经知道了它的用法。那它能判断出所有数据的类型吗?我们在上一篇文章中提到过,我们再来回顾一下。

对于原始类型,当我们用它来判断除了null以外的类型时,它都能判断成功。而对于null,它返回的却是对象。

image.png

我们在上一篇文章解释了原因。如有遗忘,可以回去巩固。

[>](深入剖析JS的内存机制我们已经学过了JS的执行机制、作用域链和闭包。今天我们来好好聊聊JS的内存机制。当我们写下一段JS - 掘金)

而对于引用类型。它会把引用类型都判断成对象。

image.png

除了函数,函数有点特别,typeof能正确判断。

image.png

所以对于typeof,我们有个结论:typeof 可以准确的判断除了null之外的所有原始类型,不能判断引用类型(除了function)。

3.instanceof

既然typeof能判断原始类型,那应该也会有个方法能判断引用类型。那就是instanceof。这个方法就有意思了,我们先来认识一下它,再把它的原理搞清楚。

3.1 instanceof的用法与特点

我们来见识一下它的用法。

console.log({} instanceof Object);

instanceof前面放要进行判断的数据类型,后面放数据类型。它和typeof不一样。typeof是如果能读得懂你是什么类型,直接返回你的类型。而instanceof是用来判断你是不是隶属于某一种类型,返回true or false。

image.png

对于引用类型,它都能准确的判断。

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的呀,应该判断成功才对。

image.png

结果却是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。

image.png

这是为什么呢?这就不得不聊到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。

image.png

确实是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,结果会是什么呢?

屏幕截图 2024-11-25 225727.png

它返回的是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。

我们来运行一下看看结果。

image.png

给我们返回了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,这应该报错才对。

image.png

但结果却是undefined。

其实v8引擎在读到这段代码时,它会进行这样一个操作。let num = 123会执行成let num = new Number(123)创建一个实例对象,然后读到num.a = 1这行代码时,确实往实例对象上增加了这个属性。

在读取值的时候V8引擎就会执行第一条定理:原始类型不能拥有属性和方法,属性和方法只能是引用类型。 所以v8引擎转念一想,用户定义的是一个字面量,是一个原始类型,我们就不能让它拥有属性和方法。于是它就delete num.a,将这个属性删除。所以去访问时得不到值。

image.png

而当我们去访问对象上不存在的属性时,它不会报错,会输出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。

屏幕截图 2024-11-26 102418.png

因为此时我们用new去调用了构造函数创建出来了num,说明用户确实是相把num当做对象去使用,所以能输出成功。

当我们用new去调用一个构造函数时,得到的就是一个包装类。

3.5 instanceof的总结

现在,我们已经知道了instanceof的用法与特点,还有执行原理。它可以判断引用类型,不能判断原始类型。它是按照原型链去判断的。

所以对于instanceofinstanceof 通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)

4. Object.prototype.toString.call(x)

还有一种方法,它既能判断原始类型又能判断引用类型。它就是Object.prototype.toString.call(x)。我们来看看它是怎么用的。

let a = 1
console.log(Object.prototype.toString.call(a))

image.png

它能成功判断a为Number类型。

let b = {}
console.log(Object.prototype.toString.call(b))

image.png

它也能成功判断引用类型。

那么它的原理是什么呢?

我们在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()

  1. 如果 this 值为 undefined,则返回 “[object Undefined]”。
  2. 如果 this 值为 null,则返回 “[object Null]”。
  3. 设 O 为调用 ToObject 的结果,将 this 值作为参数传递。
  4. 设 class 为 O 的 [[Class]] 内部属性的值。
  5. 返回 String 值,该值是连接三个字符串 “[object ”、class 和 “]” 的结果。

因为我们让Object.prototype.toString的this指向了x,所以此时的this就为x。

看第一和第二条,当this值为undefined或null时,他能返回‘[object Undefined]’或‘[object Null]’,也就是成功判断了undefined和null的数据类型。

image.png

那第三、四、五条是什么意思呢?

第三条,将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));

image.png

只有数组身上有这个方法。

6. 总结

至此我们已经学完了JS中所有进行内存判断的方法。

  1. typeof 可以准确的判断除了null之外的所有原始类型,不能判断引用类型(除了function)

  2. instanceof 通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)

  3. Object.prototype.toString.call(x)借助Object原型上的toString方法在执行过程中会读取x的内部属性[[class]]这一机制

  4. Array.isArray(x)