手撕-JS 类型判断

507 阅读6分钟

JS作为一门弱类型语言,其判断类型的方式有很多种,且对于不同类型的数据有不同的方式判断,本文总结一下哪些场景应用什么方式判断。

JS数据类型

在说判断类型之前,先总结一下JS都有哪些数据类型,一般分为基本数据类型引用数据类型

基本数据类型:null、undefined、string、number、boolean、symbol、bigint

引用数据类型:object、array、function

准确来说,JS有8种内置类型,即7种基本数据类型以及一种引用数据类型对象(object),像ArrayDate...这种称之为内置对象。

注意:类型是小写字母开头,大写字母开头的是构造函数!!!

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"

其实在JSNaN本身就是一个number,其是Not a Number的英文缩写,譬如当使用运算符出现了一些不可预期的问题时就会返回NaN,譬如:

2 * 'Madman'  // NaN

typeof 5*1 // NaN 

因为typeof 的优先级高于运算符,所以上面的相当于(typeof 5) * 1

typeof 为什么判断不了引用数据类型?

这个问题其实牵涉的比较复杂,上文中提到书里说typeof 判断时其实走的是二进制,前三位为0的就会归为objectnull的二进制全是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

因为instanceofconstructor都受限于顶层对象,而前者并没有此限制。

就是比如一个页面通过iframe加载另一个页面,两个页面的构造函数不是同一个引用,因为是属于两个顶层对象下。

虽然一般也没人这么用=o=。

Object.prototype.toString.call()

上述中的各种判断其实都有自己的利弊,我们可以通过一个相对完美的方式去判断各种类型。

Object.prototype._isType = function(constructorStr) {
    return Object.prototype.toString.call(this) === `[object ${constructorStr}]`
}