前言
Hello!小伙伴们,准备了那么久,终于开始我的第一篇大作了,哈哈~
当我们封装一些工具类方法,需要对参数类型严格判断的时候,往往会用到类型判断,下面我介绍几种实用的类型判断方式。
走过,路过,不要错过!
在ECMAScript规范中,共定义了7种数据类型,对于它的分法也是多种多样,下面我总结了一种简单清晰的分类方法:(注:Bigint将会成为一种新增数据类型,这里暂不介绍)
- 基本类型(简单类型/值类型/原始类型):布尔、数值、字符串、undefined、null、Symbol
- 引用类型(复杂类型):Object
- 基本包装对象:Boolean、Number、String
- 单体内置对象:Array、Function、Date、RegExp
- 宿主环境对象:浏览器环境-HTMLDocument、Window;Node环境-global等
1. typeof 操作符
typeof 操作可以判断基本类型的数据,但是也存在一些特例,比如typeof null 返回的是“object” ,因为 从逻辑上,null 这个特殊值被认为是一个对空对象的引用,表示一个空对象指针。
typeof 5 // "number"
typeof true // "boolean"
typeof 'text' // "string"
typeof Symbol(1) // "symbol"
typeof undefined // "undefined"
typeof null // "object"
注意:因为 typeof 是一个操作符而不是函数,所以不需要参数,但是可以使用参数,执行结果是一样的
let message = "some string"
typeof message // "string"
typeof(message) // "string"
对于引用类型的数据,typeof 判断的结果均为 "object"
typeof [] // "object"
typeof new Date() // "object"
综上所述:typeof 只能准确判断除 null 以外的基本类型
2. instanceof 运算符
instance 翻译过来是“实例”的意思,顾名思义,instanceof 是用来 判断数据是否是某个对象的实例,返回一个布尔值。
基本用法
比如 obj instanceof Object 判断的是,即 obj 是否为 Object 的实例。
function Person(name) {
this.name = name
}
const p = new Person('sunshine')
p instanceof Person // true
// 这里的 p 是 Person 函数构造出来的,所以顺着 p 的原型链可以找到Object的构造函数
p.__proto__ === Person.prototype // true
p.__proto__.__proto__ === Object.prototype // true
缺点
- 对于基本类型的数据,instanceof是不能直接判断它的类型的,因为实例是一个对象或函数创建的,是引用类型,所以需要通过基本类型对应的 包装对象 来判断。所以对于 null 和 undefined 这两个家伙就检测不了了~
5 instanceof Number // false
new Number(5) instanceof Number // true
- 因为原型链继承的关系,instanceof 会把数组都识别为 Object 对象,所以用 Object.prototype.toString.call(xxx) 方法会更准确一点。下面会对
Object.prototype.toString做出详细讲解。
let arr = [1, 2]
console.log(Object.prototype.toString.call(arr) === '[object Array]') // true
console.log(arr instanceof Array) // true
console.log(arr instanceof Object) // true
- 如果网页里面有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
模拟instanceof
instanceof的核心原理是:用来检测某个实例对象的原型链上是否存在构造函数的 prototype 属性,下面简单模拟一下 :
function myInstanceof(instanceObj, constructorFun) {
const prototypeObj = constructorFun.prototype // 获取构造函数的原型对象(显示原型)
instanceObj = instanceObj.__proto__ // 获取实例对象的原型(隐式原型)
while (instanceObj) {
if (prototypeObj === instanceObj) {
return true
}
instanceObj = instanceObj.__proto__ // 重点:层层向上遍历
}
return false
}
myInstanceof('', String) // true
myInstanceof(1, String) // false
function Person(name) {
this.name = name
}
const p = new Person('sunshine')
myInstanceof(p, Person) // true
小结
- instanceof 可以检测所有能转换为实例对象的数据
- instanceof 判断数组不严谨
- instanceof 判断必须在当前页面声明
3. constructor
使用constructor 可以查看目标构造函数,也可以进行数据类型判断。但是不能判断 null 和 undefined,因为这两个特殊类型没有其对应的包装对象。constructor和instanceof 类似,都必须在当前页声明。
// 以下方法均返回true
(5).constructor === Number
"text".constructor === String
true.constructor === Boolean
({}).constructor === Object
// Uncaught TypeError: Cannot read property 'constructor' of undefined
undefined.constructor === undefined
null.constructor === null
4. Array.isArray()
一个经典的ECMAScript问题是判断一个对象是不是数组。在只有一个网页(一个全局作用域)的情况下,使用instanceof足矣。如果网页里面有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
为了解决这个问题 ECMAScript提供了 Array.isArray() 方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。
5. Object.prototype.toString
在判断数据类型时,我们称 Object.prototype.toString 为 “万能方法” “终极方法”,示例代码如下:
// 简单实现
var arr = [1, 2, 3]
Object.prototype.toString.call(arr)
// 封装实现
function type(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
function isArray(obj) {
return type(obj) === 'array'
}
console.log(isArray(arr)) // true
深度剖析
在ECMAScript中,Object类型的每个实例都有toString()方法,返回对象的字符串表示,所以每个实例化的对象都可以调用toString()方法。调用结果如下:
var obj = {a: 1}
obj.toString() //"[object Object]"
那么,obj的toString()方法是哪里来的呢?
我们顺着原型链,obj => obj.proto => Object.prototype,可以发现,toString()方法是定义在Object的原型对象Object.prototype上的,这样Object的每个实例化对象都可以共享Object.prototype.toString()方法。
如果不通过原型链查找,直接调用Object.prototype.toString()也可以,因为Object.prototype也是对象,所以返回了对象的字符串表示。通过obj对象调用Object.prototype.toString()方法的正确方式如下所示:
Object.prototype.toString.call/apply(obj) // "[object Object]"
接下来,我们再来分析一下不同类型的“对象”调用toString()方法,返回值有什么不同之处?
Object作为引用类型,它是一种数据结构,常被称为Object类(但这种称呼并不妥当,JS中没有类,一切都是语法糖而已)。另外,基于Object类型,JS还实现了其他常用的对象子类型(就是不同类型的对象),我们可以说,Object类是所有子类的父类。
所以,上文提到的定义在Object.prototype上的toString()方法,可以说是最原始的toString()方法了,其他类型都或多或少重写了toString()方法,导致不同类型的对象调用toString()方法产生返回值各不相同。
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call() // "[object Undefined]"
Object.prototype.toString.call(new Date()) // "[object Date]"
Object.prototype.toString.call(/at/) // "[object RegExp]"
引申知识点:纯数字不可以直接调用toString方法,因为.也可以代表数学中的小数点,js执行引擎在做语法解析的时候,会通过.分隔符来解析上下文,但是对于 1.toString() 这样的表达式,会理解为不合理的小数,故而报错。如果想解决这个问题,可以在1外面加上(),即 (1).toString()
1.toString() // Uncaught SyntaxError: Invalid or unexpected token
(1).toString() // "1"
小结
toString()方法是定义在Object的原型对象Object.prototype上的,Object的每个实例化对象都可以共享Object.prototype.toString()方法,可以说Object.prototype.toString.call(xxx) 是类型判断的终极解决方案
面试题
如何判断一个对象是否为数组?
// 1.判断是否属于数组实例
[] instanceof Array === true
// 2. 通过对象的原型方法判断
Object.prototype.toString.call(arr) // es3
// 3. 判断值是不是数组
Array.isArray([])
// 4. constructor
[].constructor === Array
以上几种方式都是最简单的判断数据类型的方式,不难发现,在我们开发过程中,很多开源框架和第三方库中都使用了这些方式,下面摘抄了两段,供大家参考。
Iview源码
function typeOf(obj) {
const toString = Object.prototype.toString;
const map = {
'[object Boolean]' : 'boolean',
'[object Number]' : 'number',
'[object String]' : 'string',
'[object Function]' : 'function',
'[object Array]' : 'array',
'[object Date]' : 'date',
'[object RegExp]' : 'regExp',
'[object Undefined]': 'undefined',
'[object Null]' : 'null',
'[object Object]' : 'object'
};
return map[toString.call(obj)];
}
element-ui源码
export function isString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}
export function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function isHtmlElement(node) {
return node && node.nodeType === Node.ELEMENT_NODE;
}
export const isFunction = (functionToCheck) => {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
};
export const isUndefined = (val)=> {
return val === void 0;
};
export const isDefined = (val) => {
return val !== undefined && val !== null;
};
vue源码
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
export function isTrue (v: any): boolean %checks {
return v === true
}
export function isFalse (v: any): boolean %checks {
return v === false
}
/**
* Check if value is primitive.
*/
export function isPrimitive (value: any): boolean %checks {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
/**
* Quick object check - this is primarily used to tell
* Objects from primitive values when we know the value
* is a JSON-compliant type.
*/
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
/**
* Get the raw type string of a value, e.g., [object Object].
*/
const _toString = Object.prototype.toString
export function toRawType (value: any): string {
return _toString.call(value).slice(8, -1)
}
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
export function isRegExp (v: any): boolean {
return _toString.call(v) === '[object RegExp]'
}
最后
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。
阅读更多文章,可关注我的微信公众号【阳姐讲前端】,每天推送高质量文章,我们一起交流成长。