JS
作为一门弱类型语言,其判断类型的方式有很多种,且对于不同类型的数据有不同的方式判断,本文总结一下哪些场景应用什么方式判断。
JS
数据类型
在说判断类型之前,先总结一下JS
都有哪些数据类型,一般分为基本数据类型和引用数据类型。
基本数据类型:null、undefined、string、number、boolean、symbol、bigint
引用数据类型:object、array、function
准确来说,JS有8种内置类型,即7种基本数据类型以及一种引用数据类型对象(object
),像Array
、Date
…这种称之为内置对象。
注意:类型是小写字母开头,大写字母开头的是构造函数!!!
typeof
typeof 判断基本数据类型
一般来说,typeof
用来判断基本数据类型:
let unde = undefined
typeof unde // "undefined"
typeof Madman // "undefined"
let str = 'Madman'
typeof str // "string"
let num = 1
typeof num // "number"
let boole = true
typeof boole // "boolean"
let sym = Symbol('id')
typeof sym // "symbol"
let bigi = 10n
typeof bigi // "bigint"
复制代码
typeof null
上面说了,只是一般,像比较特殊的null
:
let nu = null
typeof nu // "object"
复制代码
why ?
有的书里说,
null
指向了一个空对象的指针,所以typeof
判断会返回object
。《JavaScript高级程序设计(第2版)》
有的书里还说,
javascript中不同对象在底层都表示为二进制,而javascript 中会把二进制前三位都为0的判断为object类型,而null的二进制表示全都是0,自然前三位也是0,所以执行typeof时会返回 ‘object’。——《你不知道的javascript(上卷)》
大家自行判断,这里就不争辩这个问题了,那如何判断 null
呢?
let nu = null
nu === null // true
复制代码
typeof number
除了null
,还有一个非一般的,那就是number
,虽然上述中用typeof
判断的 num
确实返回了"number"
,但是下面的代码也表现了它的非一般性。
let num = 1
typeof num // "number"
let na = NaN
typeof na // "number"
复制代码
其实在JS
中NaN
本身就是一个number
,其是Not a Number的英文缩写,譬如当使用运算符出现了一些不可预期的问题时就会返回NaN
,譬如:
2 * 'Madman' // NaN
typeof 5*1 // NaN
复制代码
因为typeof
的优先级高于运算符,所以上面的相当于(typeof 5) * 1
;
typeof
为什么判断不了引用数据类型?
这个问题其实牵涉的比较复杂,上文中提到书里说typeof
判断时其实走的是二进制,前三位为0的就会归为object
,null
的二进制全是0,所以用它判断对象或者数组都会返回object
,当然这种理解都是源于书上,大家有什么好的解释也可以一起分享一下,这里不做过多深究。
typeof fn
为什么返回function
?
细心的搬运工会发现,在我们阅读一些源码的时候,判断一个函数的是什么类型,往往使用的是type fn === 'function'
,又和上面的观点有出入,function
不是属于引用数据类型吗?
function实际上是object的一个“子类型”。具体来说,函数是“可调用对象”。 ——你不知道的JavaScript中卷
嗯,这里又引用了“书上说”,=v=,那么既然被规定为一个可调用对象,它必然有和对象的区别,在查阅相关资料会发现,
Function 会按照 ECMA-262 规范实现 [[Call]],
那么是不是typeof
在判断对象的时候,如果内部实现了[[call]]就会返回'function'
呢?这个论证这里也不再深究,对于一个没研究过浏览器实现原理的搬运工来说,这只是我的一个观点,大家有什么好的观点也可以发表一下,互相学习。
文章开头说到,小写的是类型,大写的是构造函数,一定要明白这一点。
// 下面的判断都会返回'function'
typeof Number
typeof String
typeof Function
typeof Array
typeof Object
typeof class A {}
// 下面两个大家理解一下
typeof Null
typeof Undefined
/**
* 会返回 'undefined',别问为啥,Null、Undefined这里就是被识别成一个未声明定义的变量,
* null和undefined不存在构造函数
*/
复制代码
Number.isNaN() 、 isNaN()
typeof + Number.isNaN()
准确判断数字类型
上文中说到typeof 并不能准确的判断一个变量是否是我们认知中的数字类型,那现在我们通过别的方式实现一个可以准确判断认知中的数字类型的。
function _isNum(num) {
return typeof num === 'number' && !Number.isNaN(num)
}
复制代码
Number.isNaN()
:用于确定传递的值是否为 NaN
,并且检查其类型是否为 Number
isNaN()
:也是用来判断值是否为NaN
,和上面不同的是,如果传递的值不是number
类型,会使用Number()
进行强转后判断。来试着实现一个polyfill
:
var isNaN = isNaN || function (v) {
var n = Number(v)
return n !== n
}
复制代码
上述中使用了NaN
的一个特性,就是自身永远不等于自身。
用isNaN()
实现一个Number.isNaN()
:
Number.isNaN = Number.isNaN || function(v) {
return typeof v === 'number' && isNaN(v)
}
复制代码
instanceof
用来判断左边的对象实例的原型链上是否存在右边构造函数的prototype
原理
let date = new Date()
date instanceof Date // true
date instanceof Object // true
let fn = () => {}
fn instanceof Function // true
fn instanceof Object // true
let arr = []
arr instanceof Array // true
arr instanceof Object // true
let o = {}
o instanceof Object // true
class A {}
let a = new A()
a instanceof A // true
a instanceof Object // true
class A_1 extends A {}
let a_1 = new A_1()
a_1 instanceof A_1 // true
a_1 instanceof A // true
a_1 instanceof Object // true
复制代码
通过上面的代码,总结一下规律,我们来实现一个简版的instanceof
:
Object.prototype._instanceof = function (constru) {
let consProto = constru.prototype
let currentProto = this.__proto__
while(true) {
if (currentProto === null) return false
if (currentProto === consProto) return true
currentProto = currentProto.__proto__
}
}
复制代码
注意: Object.prototype.__proto__
虽然被广大浏览器支持,且此属性已被ES6
规范中标准化,但是规范里仍不建议使用此属性,我们可以使用Object.getPrototypeOf()
代替它。
为什么instanceof
判断不了基本类型?
先看看下面的代码:
let n = 2
n instanceof Number // false
n instanceof Object // false
let n_n = new Number(2)
n_n instanceof Number // true
n_n instanceof Object // true
let str = 'Madman'
str instanceof String // false
str instanceof Object // false
let str_str = new String('Madman')
str_str instanceof String // true
str_str instanceof Object // true
复制代码
我们知道创建变量常用的两种方式:一个是字面量,一个是通过构造函数。
像直接通过字面量声明的变量我们称之为“原始值”,原始值是没有任何属性和方法的,只有当起被使用时才会被装箱。所以一般不使用instanceof
判断这种基本数据类型。
constructor
此属性是被挂载到Object.prototype上的,它返回的是创建实例对象的构造函数的引用。
let n = 2
n.constructor === Number // true
let str = 'Madman'
str.constructor === String // true
let date = new Date()
date.constructor === Date // true
let fn = () => {}
fn.constructor === Function // true
let arr = []
arr.constructor === Array // true
let o = {}
o.constructor === Object // true
class A = {}
let a = new A()
a.constructor === A // true
class A_1 extends A {}
let a_1 = new A_1()
a_1.constructor === A_1 // true
a_1.constructor === A // false
复制代码
看起来很 nice 啊,但是这种方式有个最大的问题就是除了string、number、boolean类型的constructor属性是只读的,别的类型的constructor属性是可以被修改的!!!,
let date = new Date()
date.constructor = () => {console.log('我被修改过了')}
date.constructor === Date // false
复制代码
当然,一般没人会闲得去改它=0=。
Array.isArray()
此函数用来判断一个对象是不是数组。
let arr = []
Array.isArray(arr) // true
复制代码
当需要判断一个对象是不是数组的时候,优先使用Array.isArray
而并非instanceof
。
因为instanceof
和constructor
都受限于顶层对象,而前者并没有此限制。
就是比如一个页面通过iframe加载另一个页面,两个页面的构造函数不是同一个引用,因为是属于两个顶层对象下。
虽然一般也没人这么用=o=。
Object.prototype.toString.call()
上述中的各种判断其实都有自己的利弊,我们可以通过一个相对完美的方式去判断各种类型。
Object.prototype._isType = function(constructorStr) {
return Object.prototype.toString.call(this) === `[object ${constructorStr}]`
}
复制代码