目录标题
typeof
什么 typeof
在JavaScript中,
typeof
是一个操作符,用于检测一个值的数据类型,它可以检测出大部分的基本类型和引用类型,返回一个字符串表示该值的类型,可以检测 String、Object、undefined、Number、Function、Symbol、Bigint 类型
typeof 的使用
-
我们来看一下 typeof 的使用,当然,可能下面的例子看完之后会有一些疑惑,我后面都会一一解析,案例如下:
console.log(typeof 123) // number console.log(typeof '123') // string console.log(typeof false) // boolean console.log(typeof {}) // object console.log(typeof null) // object console.log(typeof undefined) // undefined console.log(typeof []) // object console.log(typeof Symbol()) // symbol console.log(typeof BigInt(123)) // bigint console.log(typeof function () {}) // function
typeof null 为什么是 “object”
为什么返回的是 “object”,这是 JavaScript 的一个历史遗留问题,在早期的 JavaScript 中,null 是用来给对象占位的,
表示是一个空对象
,而且对象在内存中的表示是以 32 位二进制数字的形式存储的,对象的在底层中二进制表示前三位都是 0,而在这种二进制的表示法中,null 的值会被存储为全是 0 的二进制数字
,也因此当使用 typeof 检测 null 时,它会返回 “object”,表示 null 是一个特殊的空对象指针
typeof [] 为什么是 “object”
至于为什么检测数组时返回的是一个 “object” 呢,因为 typeof 在检测的这里检测是一种引用值,也就是说只要这里是引用类型就会返回 “object”,我们来测试一下,如下:
-
console.log('对象:', typeof {}) // 对象: object console.log('数组:', typeof []) // 对象: object console.log('Set:', typeof new Set()) // 对象: object console.log('Map:', typeof new Map()) // 对象: object console.log('Date:', typeof new Date()) // 对象: object console.log('RegExp:', typeof new RegExp()) // 对象: object
-
其实看到这里你应该有一个疑惑,Function 不也是属于引用类型吗?就它特殊是吧,对,就它特殊
typeof function foo(){} 为什么是 “function”
在 JavaScript 中,函数是一种特殊的对象类型(函数对象),具有一些特殊的行为和属性,其中被区别于其他对象的点在于,函数是可以调用的,也因此
函数和对象虽然都是引用类型
,但是又因为具备这不同的属性和行为,函数在定义的时候就会被定义称为一种特殊的对象类型,即函数对象
,所以为了区别函数对象类型和其他对象类型,JavaScript 在 typeof 运算符中将函数对象的类型识别为 "function"
-
从上面的概述中我们可以总结出一个结论,当一个
对象具备可调用的属性时
,它就是一个函数对象
,函数对象在被 typeof 检测时就会返回 function
,我们可以来具体的检测一下,如图:
-
注意了,上面的这里我们并没有对他们进行 new 关键字的调用,因此就不会创建一个实例化的对象,也就是说 typeof 检测的是它们本身,它们本身为函数对象就会返回 “function”
-
本着严谨的角度,我们在将上面例子中的函数对象进行实例化测试一下,如图:
-
这时候输出 object 是因为他们经过实例化之后已经是一个
普通的函数对象
了 -
这时候你可能会问那我在使用 typeof 检测 new Function() 的时候它返回也是 “function” 啊,这是因为 Function 这个构造函数它非常之特殊,特殊到什么地步呢,它在 JavaScript 运行之初就在内存当中了,我说我是 “function” 我就是 “function”,如果还要纠结这一点的我觉得你可能需要和语言创始人对线一手
typeof 总结
- 经过上述的解析,相信你对 typeof 有了更深刻的理解,现在我们也可以更好的解释一下为什么数组和对象被 typeof 检测时都是返回 “object”,因为
函数对象经过 new 运算符的实例化
之后,都会得到一个普通对象
,虽然数组可以被索引值所获取,但是本身都是由一个函数对象实例化之后得到的普通的对象,只是这个对象更加具备一些特殊性而已 - 至于为什么 undefined 返回的是一个 “undefined”,这是因为 undefined 表示的是
无值
,是一种其他的数据类型
- typeof是一个一元运算符,它的作用是返回一个表示
数据类型的字符串
- 需要注意的是,
typeof运算符并不能准确地区分数组和普通对象
,因为它们都被认为是 “object” 类型
Object.prototype.toString.call()
如果需要理解 Object.prototype.toString.call() 需要对 JavaScript 中原型链的知识有一定的理解,如果这方面不太了解的可以观看我的另一篇文档
文档地址:深入理解JavaScript之原型链
为什么 Object.prototype.toString.call() 可以判断数据类型
-
因为它利用了JavaScript的一个特性,
每一个对象都有 toString() 方法
,这个方法的作用是将对象转换为字符串类型,我们可以测试一下,如下:const obj = { a: 1, b: 2 } console.log(obj.toString()) // [object Object] console.log(typeof obj.toString()) // string const arr = ['aaa', 'bbb', 'ccc'] console.log(arr.toString()) // aaa,bbb,ccc console.log(typeof arr.toString()) // string const str = 'Hello World' console.log(str.toString()) // Hello World console.log(typeof str.toString()) // string const flag = true console.log(flag.toString()) // true console.log(typeof flag.toString()) // string
-
通过上面举例的部分不知道你们有没有发现一个现象,好像
只有 Object 使用 toString() 方法
时返回了一个[object Object]
,其余的数据类型都是返回了一个具体的字符串的值,而且好巧不巧这个 [object Object] 字符串这个字符串由"[object "和调用对象的类型名组成
,表示是一个 Object 类型 -
Object.prototype.toString() 这个方法会返回一个
描述该对象的字符串
,其中包含了"[object Object]"这样的具备数据类型的字符串
,而当调用其他内置对象类型的 toString() 方法时,会根据具体的类型名返回相应的字符串 -
至于这个方法为什么可以返回一个具备数据类型的字符串,这是 JavaScript 内部的机制实现的,并不需要我们关心
为什么其他对象身上的 toString 方法不可以
-
按照我们上面的理论来说,那数组这些对象在调用 toString() 方法的时候也应该返回一个具备数据类型的字符串啊,为什么其他对象身上的 toString() 方法不能判断数据类型呢,因为其他对象对于这个 toString() 方法进行了重写,也就是说在这些对象身上的原型上面又增加了一个 toString() 方法,然后
根据原型链的就近原则查找顺序
就会找到对象本身身上的 toString() 方法,我们可以例举一些常见的对象身上的 toString() 方法,如下:- 数字类型(Number):toString() 方法可以将一个数字转换成字符串形式,还可以指定进制,比如:二进制、十六进制
- 字符串类型(String):toString() 方法返回字符串本身
- 布尔类型(Boolean):toString() 方法返回 true 或 false 字符串
- 数组类型(Array):toString() 方法将数组转换成一个由数组元素组成的字符串,每个元素用逗号分隔
- 对象类型(Object):toString() 方法返回"[object Object]"字符串
- 函数类型(Function):toString() 方法返回函数的源代码
- 日期类型(Date):toString() 方法返回日期时间的字符串形式
- 正则表达式类型(RegExp):toString() 方法返回正则表达式的源代码
-
具体是不是我们所推测的一样呢,我们可以使用方法
hasOwnProperty()
方法来检测这些对象原型上
是否具备 toString() 方法,检测结果如图:
-
原型只有函数对象身上有,普通对象身上是没有的,因此如果我们将 Array 原型身上的 toString() 方法去掉的话,那 Array 使用 toString() 方法时发现原型上没有,然后
原型对象又是一个普通对象
,这个普通对象的隐式原型__proto__
又指向 Object 函数对象的原型对象
身上,此时发现 Object 的原型身上具有 toString() 方法,就会直接使用,而使用 Object 身上的 toString() 方法就可以判断数据类型,刚好符合我们的需求,我们可以检测一下是否真的是这样呢,如下:delete Array.prototype.toString const arr = [1, 2, 3] console.log(arr.toString()) // [object Array]
-
可以看到输出了我们所需要的数据类型,如果大家有兴趣还可以测试一下其他对象的打印结果,我这里就不在演示了
为什么需要 call 方法
-
经过上面的解析我们已经可以得出为什么 Object.prototype.toString() 方法可以判断数据类型了,为什么还需要 call 呢,我们可以不加 call 测试一下数组,看看是否能得到正确的结果,如下:
console.log(Object.prototype.toString([])) // [object Object]
-
为什么输出的是 “[object Object]” ,这是因为 Object.prototype 本身就是一个原型对象,而原型对象就是一个普通对象,所以当然会输出 “[object Object]”
-
也因此我们需要使用 call 方法来帮我们改变这个调用者,也就是改变 this 执行,关于这块的知识就不在进行讲解了,大家自行了解即可,我们可以测试一下,如下:
console.log(Object.prototype.toString.call([])) // [object Array] console.log(Object.prototype.toString.call(123)) // [object Number] console.log(Object.prototype.toString.call(function foo() {})) // [object Function] console.log(Object.prototype.toString.call('123')) // [object String]
-
现在就可以精准的判断出我们的数据类型了,而我们前面删除一个对象身上的 toString() 方法不也正是为了让一个数组在调用 toString() 方法的时候是使用 Object 原型上的 toString() 方法,而非自己本身的 toString() 方法,因此使用 call 方法可以直接帮助我们省去删除自身原型身上 toString() 方法这一步骤,算是一个另类的语法糖吧
总结
现在我相信你对于 Object.prototype.toString.call() 为什么可以判断数据类型就有很好的认知了,而非是只知其用,不知其源,而当我们需要判断一个数据类型的时候每次书写这样一段代码就显得很呆板也繁琐,因此我们可与封装一个函数,来帮助我们实现,如下
// 当 slice() 方法的第一个参数是正数,第二个参数是负数时
// - 第一个参数表示从字符串开头开始的索引位置
// - 第二个参数表示从字符串末尾开始的索引位置
function checkType(any) {
return Object.prototype.toString.call(any).slice(8, -1)
}