js中大体上有四种方法可以判断数据类型,分别为typeof、instanceof、Array.isArray()
和Object.prototype.toString.call()
在进行判断数据类型前,我们需要复习下数据有哪些类型。
js数据类型可以分为两大类,原始数据类型和引用数据类型
-
原始数据类型:
数字,字符串,布尔,undefined,null,BigInt,Symbol
-
引用数据类型:
对象,数组,函数,日期等
一、typeof
- 可以准确判断除null之外的原始数据类型
- 可以判断function
es6之后新增的两个原始数据类型bigint和symbol对于typeof来说同样可以识别。这里也顺便介绍下这两种类型
bigint
俗称大整型。js有个最大安全值为2^53,大整型就可以突破这个安全值。当然在js中书写次方是两个乘号,也就是 2 ** 53
再大就需要用上bigint了,我们只需要在数字后面加个n即可
ini
let big = 223n
symbol
symbol用于定义独一无二的值,通常不参与逻辑运算中
ini
let s1 = Symbol('hello')
let s2 = Symbol('hello')
console.log(s1 === s2) // false
当我们怕自己框架的数据取名跟别人源码中数据取名相同的时候我们就可以用symbol来定义
有小伙伴留言问到== 和 === 的区别,这里我也顺便带过一下
== 和 ===
== 是用来判断值是否相等,===是用来判断值和数据类型是否相同
引用数据类型中===还需要比上一个指针,也就是地址
ini
console.log(1 == '1') // true 这里是因为数据类型转换,比较值时字符串会被转为数字
console.log(1 === '1') // false
typeof判断逻辑
这里把我之前原型那期挂下面,文章最后介绍了typeof的判断逻辑
面试官真烦,问我这么简单的js原型问题(小心有坑) - 掘金 (juejin.cn)
typeof的底层逻辑就是将数据类型都转换成二进制,引用类型前三位都是0,当初设计二进制时,设计师忘记了null这个类型,全都是0,因此,null也被误读成了对象。
当然这有个特例,typeof虽说不能识别出具体引用类型,但是它可以识别出函数
javascript
console.log(typeof Function()) // function
二、instanceof
- 只能判断引用数据类型
- 通过原型链查找
javascript
// 定义四种引用类型
let obj = {}
let arr = []
let fn = function(){}
let date = new Date()
// 进行判断
console.log(obj instanceof Object); // true
console.log(arr instanceof Array); // true
console.log(fn instanceof Function); // true
console.log(date instanceof Date); // true
这么看instanceof
好像确实可以判断引用类型,但是请看下面,我试试看arr和fn是不是对象
javascript
console.log(arr instanceof Object); // true
console.log(fn instanceof Object); // true
额......好像也没毛病,数组和函数确实都是一种特殊的对象,但是instanceof
是怎么知道的呢?其实这里就是因为instanceof
判断是通过原型链的,接下来讲的内容建议大家先看下上面放出的原型文章。
arr是个空数组,其实就相当于new Array(),arr所以是一个实例化对象。
a instanceof b
在判断的时候,会先判断a的隐式原型是否等于b的显示原型,如果不等于a就会继续沿着原型链深入。拿arr为例,arr的隐式原型就是构造函数Array的显示原型,然后构造函数Array也是个对象,他被Object实例出来的,所以构造函数Array的隐式原型就是Ojbect的显示原型,匹配成功返回true
所以arr instanceof Object
的查找是下面这样
javascript
arr.__proto__ 是否为 Object.prototype // 否
arr.__proto__.__proto__是否为Object.prototype // 是 返回true
a instanceof b
的a最终会沿着__proto__
找到object的null值(object.__proto__ == null
)
因此我们可以自己模范一个instanceof
的函数,如下
scss
function instanceOF(L, R){
// 判断左边的隐式原型是否为右边的显示原型
// 如果不是,判断左边的隐式原型的隐式原始是否为右边的显示原型
// 如此循环下去,如果左边到null了,就返回false
let left = L.__proto__
let right = R.prototype
while(left !== null){
if(left == right) return true
left = left.__proto__
}
return false
}
instanceOF([],Array) // true
instanceOF([],Object) // true
instanceOF(Function(),Object) // true
无限地判断左边的隐式原型是否等于右边的显示原型 ,有些人可能觉得不用循环都可以,因为一个实例对象好像也就是a.__proto__.__proto__.__proto__
三层就到底了,用循环是因为你还要考虑到a可能自己也创造了隐式原型
考点
instanceof
也很容易被面试官问到,他会问让你手动做一个instanceof
的函数,就是上面的,也有可能让你口头说一下,如下
a instanceof b
就是a通过循环不断找a的隐式原型是否为b的显示原型,找到了就会返回true,找不到最终到一个null,返回false
三、Array.isArray()
- 只能判断是否为数组
这个方法不是Array原型身上的,是Array自带的方法,如果是原型身上的,中间会有个prototype,因此这个方法不能被实例对象去调用,必须是Array
当你用instanceof
的时候,数组也可以被认为是对象,这个方法就可以专门用来判断是否为数组
javascript
Array.isArray([]) // true
Array.isArray({}) // false
四、Object.prototype.toStrring.call()
这是js中最完美的判断数据类型的方法,但是想要理解它的原理可不简单
下面是官方文档给出的介绍
如果你能读明白js官方文档的所有内容,那你一定可以在简历上面自信的写出:精通js
官方文档地址es5.github.io/#x15.2.4.2
当toString
这个方法被调用时,将会采取下面的步骤
-
如果this的值是undefined,那么返回"[object Undefined]"
-
如果this的值是null,那么返回"[object Null]"
-
将O作为
ToObject
的执行结果 -
让class成为O内部属性[[Class]]的值
-
返回由 "[object "和 class 以及 "]" 三部分组成的字符串,class就是执行出来的类型
这里读不懂没关系,我们下面先介绍下用法
将想要读取的东西放进call中即可,既然是最好用的判断方法,我们就来看下效果
javascript
// 7种原始数据类型都能识别
Object.prototype.toString.call(123) // '[object Number]'
Object.prototype.toString.call('Hello') // '[object String]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(123n) // '[object BigInt]'
Object.prototype.toString.call(Symbol('hello')) // '[object Symbol]'
// 四种引用类型也都可以识别
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(function(){}) // '[object Function]'
Object.prototype.toString.call(new Date()) // '[object Date]'
看完我是直呼牛逼!这东西也太强大了,前面的判断方法都有缺陷,这玩意儿全给你识别出来,甚至null这个bug都行!
既然如此,我们如何去使用它呢?
判断
可以看出,它返回一个字符串类型,我们只需要把返回结果的第二个单词提取出来进行判断即可。
我们把第二个单词截取出来,使用
slice(start,end)
方法即可截取,(这个方法同样适用于字符串 )第一个参数start是起始下标,截取出来的单词的起始下标都是8,固定不变,第二个参数end如何设定呢,我们需要的截取的单词长度不一,但是有个规律,里面的单词的最后一个字母都是倒数第二个字符,因此我们填入-1即可,为何不是-2,因为这个方法的参数是左闭右开,-2的话就截取到-3那个下标去了
javascript
let s = 'hello'
function isType(s){
return Object.prototype.toString.call(s).slice(8,-1)
}
if(isType(s) === 'String'){
return true
}
这个方法好是好,但是就是判断起来会比较麻烦(悲
不知道大家有没有发现,官方给的方法是Object.prototype.toString()
而不是Object.prototype.toString.call()
,这是为什么?
关于Object.prototype.toString()
这个东西,其实也很好理解,就是object这个万物之源有个原型,object原型中有个toString
方法,如果我们不用call行得通吗
javascript
Object.prototype.toString(123) // '[object Object]'
行不通,123这个东西调用不了左边那部分,不管你输入什么类型进去都是这个输出,你就是需要call这个东西来让123调用toString
方法
那我们看看下面有什么区别
scss
(123).toString() // '123'
Object.prototype.toString.call(123) // '[object Number]'
既然如此,上面两个不应该是一种意思吗,123通过call可以调用左边的
toString
,为何这里输出一个值的类型,而不会输出'123'
?这里你也无需问为什么了,官方已经给你定死了。(123).toString()
其实是有两个步骤,先进行判断数据类型,然后将其转换输出,而Object.prototype.toString.call(123)
只有第一步,判断数据类型
我们现在知道了,Object.prototype.toString()
这个方法需要通过call来调用才行得通,我的文章js中this究竟指向哪里?现在终于搞定了! - 掘金 (juejin.cn)中也讲过call的作用。比如下面的foo.call(obj)
就是把foo的this指向了obj这个对象
javascript
function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2
// foo中this本应指向全局(因为默认绑定或者说foo定义在全局中,它的词法作用域为全局),但是被call给改到obj中去了
call源码
call的用法
javascript
var obj = {
a: 1
}
function foo(){
console.log(this.a);
}
foo.call(obj) // 1
我们不妨再看下隐式绑定
javascript
var obj = {
a: 1,
fn: foo
}
function foo(){
console.log(this.a);
}
obj.fn() // 1
这里你做一个比较,肯定会猜到call就是利用了隐式绑定的规则,它将foo这个函数体挂在了obj中,成为了一个属性,然后再利用obj这个对象去调用它,使其成为一个隐式绑定,导致this指向改变
有了这一想法,我们其实就可以自己做一个call的源码,实际上call可以接收多个参数,我们也要考虑进去
javascript
var obj = {
a: 1
}
function foo(a,b){
console.log(this.a,a+b);
}
// call是所有函数原型自带的方法,因此我们这里也需要这样去创建
Function.prototype.myCall = function(context){
// call左边必须是函数,必须是函数去调用它,不是就抛出一个错误
if(typeof this !== 'function'){
throw new TypeError('myCall is not a function')
}
// Array.from类数组转成数组,类数组没有数组的slice方法,因此要转一下。args就是接收的形参,因为第一个参数是对象,把他切割掉,其余就是函授接收的形参,并且不影响原数组
let args = Array.from(arguments).slice(1)
// let args = [...arguments].slice(1)
// 这里的this就是传进来的函数,往对象属性中挂函数声明,隐式绑定!
context.fn = this
// 调用函数,这些参数必须还给foo函数本身,当然接受的形参必须解构掉,不能是个数组
let res = context.fn(...args)
// 触发隐式绑定后删掉,否则对象多了个函数key啊
delete context.fn
// 防止foo也会有个返回值,这样写foo没有return也没关系
return res
}
foo.myCall(obj,1,2) // 1 3
console.log(obj) // { a: 1 }
这里的解释我基本上都放在代码的注释中去了,一定要看仔细了
arguments
foo中函数的形参你无法知道有几个参数,因此我们用上arguments这个关键字,它是函数中形参的统称,括号中的形参都可以不用写了
scss
function foo(){
console.log(arguments)
}
foo(1,2,3) // Arguments(3) [1, 2, 3]
arguments是个类数组,后面我们再来详细讲解下类数组,类数组只有数组的下标,和长度,没有数组的那些方法,你可以理解为是一个阉割版的数组,其本质上还是个对象。所有的函数都有这个关键字,arguments[0]是第一个形参
Array.from
将类数组转换成数组,否则类数组用不了slice方法,类数组也可以被解构成数组,所以上面的代码let args = Array.from(arguments).slice(1)
可以写成let args = [...arguments].slice(1)
数组的解构
这是es6新增的写法,后面文章我还会提到这个
javascript
let arr = [1, 2, 3]
console.log(...arr) // 1 2 3
解构也很好理解,直接把数组的值都解剖出来
考点
面试官可能会问你Object.prototype.toString.call()
为什么可以准确判断数据类型,或者说问你这个方法有了call为何就不一样,你只需要把call的源码逻辑告诉他就可以,其实就是一个隐式绑定。
比如
Object.prototype.toString.call(num)
就是call把左边的toString搬到了右边num里面,然后让num去执行toString方法,当然这里是识别出数据类型,toString本身是不接受参数的,call把toString搬到了num身上并且去调用而已
总结
typeof
是用来判断原始数据类型的(除null这个bug外),引用类型只能判断一个函数。instanceof
只能判断引用类型,但是判断数组,函数不准,因为他是通过原型链来判断的。Array.isArray()
是Array
自带的方法,只能判断数组。Object.prototype.toString.call(***)
可以判断所有的数据类型,但是判断起来会麻烦一点,并且call的作用就是把toString方法搬到了***
中,让***
去调用toString
方法
如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]