JavaScript专题[1]-数据类型判断

238 阅读8分钟

前言

Hello!小伙伴们,准备了那么久,终于开始我的第一篇大作了,哈哈~

当我们封装一些工具类方法,需要对参数类型严格判断的时候,往往会用到类型判断,下面我介绍几种实用的类型判断方式。

走过,路过,不要错过!

在ECMAScript规范中,共定义了7种数据类型,对于它的分法也是多种多样,下面我总结了一种简单清晰的分类方法:(注:Bigint将会成为一种新增数据类型,这里暂不介绍)

  1. 基本类型(简单类型/值类型/原始类型):布尔、数值、字符串、undefined、null、Symbol
  2. 引用类型(复杂类型):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

缺点

  1. 对于基本类型的数据,instanceof是不能直接判断它的类型的,因为实例是一个对象或函数创建的,是引用类型,所以需要通过基本类型对应的 包装对象 来判断。所以对于 null 和 undefined 这两个家伙就检测不了了~
5 instanceof Number // false
new Number(5) instanceof Number  // true
  1. 因为原型链继承的关系,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
  1. 如果网页里面有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 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]'
}

最后

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你,欢迎点赞和关注。

阅读更多文章,可关注我的微信公众号【阳姐讲前端】,每天推送高质量文章,我们一起交流成长。