JavaScript中的类型判断方法你知道几种?
hello~好久不见,这里是哆啦美玲哈哈!最近觉得记住知识点最好的方法,还是回顾的时候写文章分享给大家最有用,虽然我写的一般,大家将就看吧。今天就是分享一个js知识点——如何判断数据类型?
数据类型
js中的数据类型分两种:基本类型和引用类型对象。
基本类型(原始类型) 包括:number、string、boolean、null、undefined、symbol、bigInt
引用类型包括 :Object、array、function、Data
类型判断的方法
1. typeof 判断原始类型
typeof 可以准确的判断除了null 之外的所有原始类型,不能准确判断引用类型,除了function
。
那typeof是怎么使用的呢?我们直接看下面的代码:
// 判断基本类型
console.log(typeof 'hello'); //string
console.log(typeof 123); // number
console.log(typeof true); // boolean
console.log(typeof null); // object
console.log(typeof undefined); // undefined
console.log(typeof Symbol(1)); // symbol
console.log(typeof 123n); // bigint
// 判断引用类型
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof new Date()); // object
console.log(typeof function foo(){}); // function
由此,我们可以发现,直接使用 typeof 关键词去判断基本类型时,只有null一种类型会被误判成为object,其他基本数据类型都能够成功判断。
其实在之前的文章中解释过: 了解JavaScript的底层——内存机制 - 掘金
这里我们再解释一次,因为JS在判断类型时,会把变量的值转换成二进制进行判断;在计算机中所有的引用类型的二进制前三位是0,而null转换成二进制全部都是0,所以被误判成是对象。此外,最特殊的函数使用typeof是可以准确的判断出是function。
2. instanceof 判断引用类型
2.1 原理
instanceof 关键字是通过原型链来判断类型相等,只能判断引用类型(原始类型没有隐式原型)
。
我们直接看代码举例:
console.log({} instanceof Object); // instanceof关键字(隶属于) 输出true
console.log([] instanceof Array); // true
console.log(new Date() instanceof Date); // true
console.log(function(){} instanceof Function); // true
console.log([] instanceof Object); // true
console.log(new Date() instanceof Object); // true
console.log(function(){} instanceof Object); // true
// console.log(null instanceof null); // 报错,右边必须是对象
console.log("hello" instanceof String); // false
console.log(123 instanceof Number); // false
console.log(true instanceof Boolean); // false
从上面的代码结果来看,数组、函数等对象,不仅能够判断它们是否隶属于他们本身的构造函数,还能判断是否是一个对象。所以instanceof关键字是判断隶属于的关系,即判断某个变量是否隶属于某种类型,返回true或者false。而在第10行中我们如果执行它,会得到报错提示:Right-hand side of 'instanceof' is not an object,这告诉我们instanceof的右边必须放一个对象才能进行判断。
但是它到底是怎么判断的呢?我们看下面一段代码:
function Car(){
this.run = 'running'
}
Bus.prototype = new Car()
function Bus(){
this.name = 'BYD'
}
let bus = new Bus();
console.log(bus instanceof Bus); // true bus.__proto__ == Bus.prototype
console.log(bus instanceof Car); // true bus.__proto__.__proto__ == Car.prototype
console.log(bus instanceof Object); // true bus.__proto__.__proto__.__proto__ == Object.prototype
我们自己创造一个构造函数Bus,并且基于Bus构建一个实例对象bus,所以13行代码返回true很好理解。然后我们又创造了一个Car的构造函数,并且让Bus的对象原型是Car的实例对象后,我们打印14行的结果是true,为什么呢?
其实,根据代码的结果,我们应该可以猜到instanceof可能是根据原型链来进行判断,我们直接来看bus、Bus和Car之间有什么关联:
首先我们回顾new的原理:将构造函数的显示原型(Object.prototype) 赋值给 实例对象的对象原型(即隐式原型obj.__ proto__)。(不懂的可以去看这篇文章:搞懂this,如此简单 - 掘金)
所以11行代码构造实例对象bus时,会实现 bus.__ proto__ == Bus.prototype。
而第五行代码Bus.prototype = new Car(),其中Bus.prototype也是一个对象,它也存在对象原型(隐式原型),所以我们人为的将构造函数Car的显示原型赋值给它的对象原型,即 Bus.prototype.__ proto__ == Car.prototype。所以我们就可以将Bus.prototype替换掉,得到 Bus.prototype._ proto_ == bus.__ proto__._ proto_ == Car.prototype。
第15行代码也同理,顺着原型链进行追溯,相信你们也可以推理得到 bus.__ proto__.__ proto__.__ proto__ == Object.prototype。所以,instanceof就是根据原型链
来判断数据是否为引用类型。
2.2 手写myInstanceof方法
前面我们已经搞清楚了instanceof的判断原理,所以我们也可以手戳一个instanceof方法,实现同理。这里我直接展示我使用的两种方法:循环和递归,代码如下:
// 方法一:while循环
function my_instanceof(L, R) {
while (L !== null) {
L = L.__proto__
if (L === R.prototype) {
return true
}
}
return false
}
console.log(my_instanceof([], Object));
// 方法二:递归
function myInstanceof(L, R) {
if (L !== null) {
L = L.__proto__
if (L !== R.prototype) {
return myInstanceof(L, R);
//每次递归的判断结果 (true 或 false) 都需要返回给上一层应该是直接返回递归的结果,
// 确保每层递归都能够返回正确的判断。
}
return true
}
return false
}
console.log(myInstanceof([],Array));
2.3 包装类
console.log(new String('hello') instanceof String); // true
console.log("hello" instanceof String); // false
我们总说let str = 'hello' 和 let str = new String('hello') 是一样的,但是为什么上面代码会出现两个结果呢?这就是我们要聊的包装类的问题。
在JavaScript中,并没有像Java中那样明确的“包装类”概念。JavaScript是一种动态类型语言,变量的类型是可以在运行时决定的,因此并不需要使用包装类来将原始数据类型(基本类型)封装成对象。然而,JavaScript仍然有一些类似的概念,尤其是与基本数据类型(例如 string、number、boolean)相关的对象封装。
-
显式创建包装对象
,即直接用对应的构造函数创建变量,会将原始数据类型包装为对象,并允许访问它们的属性和方法。例如:构建字符串对象 let str = new String('abc')。 -
基本数据类型在V8执行下会自动被封装为对象类型
。当我们访问字符串、数字、布尔值等原始类型的属性或方法时,JavaScript会自动将它们包装成相应的包装类对象。即在V8的眼里let a = 1会被执行成 let a = new Number(1)。 -
原始类型不能拥有属性和方法
,属性和方法只能是引用类型的。 -
访问对象身上不存在的属性不会报错,会是undefined
。
方便理解,我举个例子:
let num = 123 // let num = new Number(123)
num.a = 1 // {a:1}
// delete num.a
console.log(num.a); // undefined
console.log(num); // 123
代码执行的逻辑如下:
- 第一行代码在V8眼里会执行成let num = new Number(123),所以第二行代码能够往num上添加a属性。
- 我们需要的num是原始类型,不是包装类,V8严格按照原始类型不能拥有属性和方法这一原理,会将num身上的a属性移除,即delete num.a。
- 访问Number对象 num身上不存在的属性是不会报错的,会返回undefined。
- 读取值的时候会执行:读取
[[PrimitiveValue]]
的值。如下图,num内部其实有一个内置属性[[PrimitiveValue]],是只有V8能够访问的用来存取原始类型的值的属性。
上面例子是没有直接显示创建包装类的情况,如果是显示创建的情况如下:
我们会发现,显示创建包装类的情况下,V8是直接当做一个对象进行处理的,所以可以往其身上添加属性和方法。
3. Object.prototype.toString().call(x)
Object.prototype.toString().call(x)是借助Object原型上的toString方法在执行过程中会读取 X 的内部属性[[Class]]这一机制来进行数据的类型判断。
如图:
在官方文档中关于Object.prototype.toString()
触发时会执行以下步骤描述如下::
- 如果 this 值为 undefined,返回 “[object Undefined]”。
- 如果 this 值为 null,返回 “[object Null]”。
- 设 O 为调用 ToObject(C打造的V8用的方法) 的结果,并将 this 值作为参数,即V8会执行ToObject(this)。
- 设 class 为 O 的 [[Class]] 内部属性的值,即class等于O的数据类型。
- 返回 String 值,该值是将三个字符串拼接—— "[object " 、 class 和 "]"
根据上面的步骤,我总结为:Object.prototype.toString()
会读取this的值身上的内置属性[[Class]] ——对象的类型,以一个很特殊的状态返回 “[object ”、class 和 “]”
所以这个方法关键在于 this 指向谁,这就要使用call() 将this显示绑定在需要判断的对象上,就能够返回能够显示对象类型的字符串。(有不熟悉call的可以看这篇:搞懂this,如此简单 - 掘金)
测试代码如下:
let a = 1
let b = {}
console.log(Object.prototype.toString.call(a)); // [object Number]
console.log(Object.prototype.toString.call(b)); // [object Object]
4. Array.isArray() 判断数组
Array.isArray()是专门用来判断一个对象是不是数组的方法,代码如下:
let arr = []
// 判断一个对象是不是数组
console.log(Array.isArray({}));
好了,我的分享到这里就结束啦~喜欢我的分享记得给我点个赞喔!
ღ( ´・ᴗ・ `)比心,我们下次再见!!