JavaScript 发展至今,一直保持着弱类型(weakly typed)语言的特性,也就一直没有提供准确的检测数据类型的内置方法。因此开发者就不得不自己编写校验数据类型的函数方法。本文就来介绍一下,如何准确的判断 JavaScript 中的数据类型。

根据 MDN 的《JavaScript data types and data structures》这篇文档介绍,JavaScript 将数据分为了以下几个大类:

Primitive values(原始值)

其中原始值是是我们最常用的“数据”,JavaScript 有7个原始值:

利用 typeof 关键字判断原始值

原始值基本都是可以使用 JavaScript 中提供的 typeof 关键字来判断数据类型的:

let value

typeof value // -> 'undefined'
typeof false // -> 'boolean'
typeof 23 // -> 'number'
typeof BigInt(66) // -> 'bigint'
typeof 'JavaScript' // -> 'string'
typeof Symbol('prop') // -> 'symbol'

前面之所以说原始值基本都是可以用 typeof 来判断数据类型,是因为 null 是一个例外:

typeof null // -> 'object'

null 是一个对象,是不是觉得很奇怪?

typeof 关键字对判断对象(引用类型)的值

除原始值以外,JavaScript 中其它的值都应该是对象(引用)类型的值。typeof 关键对其它的值的判断几乎就没有什么做了,例如以下判断:

typeof [] // -> 'object'
typeof {} // -> 'object'
typeof function(){} // -> 'object'
typeof class {} // -> 'object'
typeof new Date() // -> 'object'
typeof new RegExp() // -> 'object'
typeof new Error() // -> 'object'

利用 typeof 判断对象类型的值,结果都一样:’object’(对象)。而作为开发者是希望知道这些“对象”具体是什么类型的“对象”。那么如何获取对象的具体类型呢?

对象(Object)类型数据的细分

如何获取对象的具体类型呢?要找到这个问题的解决方案,我们就必须知道对象(Object)类型的数据到底有哪些更细的类别分的:

完整的所有 JavaScript 内置对象请参考 Standard built-in objects

Indexed collections

Keyed collections

Structured data

Fundamental objects

Dates

Error objects

对象(Object)类型数据的判断

在了解对象的类型后,现在就需要想办法来判断这些具体的对象数据类型了。

Object.prototype.toString()

利用 typeof 判断对象类型的数据,几乎是无效的,好在我们可以使用 Object.prototype.toString() 来获取对象的具体类型的字符:

const toString = Object.prototype.toString

toString.call(null) // -> '[object Null]'
toString.call([]) // -> '[object Array]'
toString.call({}) // -> '[object Object]'
toString.call(function(){}) // -> '[object Function]'
toString.call(class {}) // -> '[object Function]'
toString.call(new Date()) // -> '[object Date]'
toString.call(new RegExp()) // -> '[object RegExp]'
toString.call(new Error()) // -> '[object Error]'
// ... 省略其它类型数据的判断 

问题是几乎已经得到了解决,是不是?

_type() 方法

根据之前的了解,可以使用 typeof 关键字判断原始值类型的数据(null除外),使用 Object.prototype.toString() 方法判断对象类型的值,因此我们可以整理一个 _type() 方法:

数据类型名称枚举值

以下代码中的 TYPES 对象中存储的就是根据 MDN 中细分的数对象类型归纳的实际开发中常见的(对象)数据类型的枚举值。

const TYPES = {
  /* ===== Primitive data types ===== */
  BIG_INT: 'bigint',
  BOOLEAN: 'boolean',
  NULL: 'null',
  NUMBER: 'number',
  UNDEFINED: 'undefined',
  STRING: 'string',
  SYMBOL: 'symbol',
  /* ===== Keyed Collections ===== */
  SET: 'set',
  WEAK_SET: 'weakset',
  MAP: 'map',
  WEAK_MAP: 'weakmap',
  /* ===== Array ===== */
  ARRAY: 'array',
  ARGUMENTS: 'arguments',
  /* ===== Typed Arrays ===== */
  DATA_VIEW: 'dataview',
  ARRAY_BUFFER: 'arraybuffer',
  INT8_ARRAY: 'int8array',
  UNIT8_ARRAY: 'uint8array',
  UNIT8_CLAMPED_ARRAY: 'uint8clampedarray',
  INT16_ARRAY: 'int16array',
  UNIT16_ARRAY: 'uint16array',
  INT32_ARRAY: 'int32array',
  UNIT32_ARRAY: 'uint32array',
  FLOAT32_ARRAY: 'float32array',
  FLOAT64_ARRAY: 'float64array',
  BIG_INT64_ARRAY: 'bigint64array',
  BIG_UINT64_ARRAY: 'biguint64array',
  /* ===== Object ===== */
  OBJECT: 'object',
  COLLECTION: 'collection',
  DATE: 'date',
  ELEMENT: 'element',
  ERROR: 'error',
  FRAGMENT: 'fragment',
  FUNCTION: 'function',
  PROMISE: 'promise',
  REGEXP: 'regexp',
  TEXT: 'text'
}

export default TYPES

Object.prototype.toString() 输出的类型名称

以下代码中的 OBJECTS 对象,是我们为了优化 _type() 方法判断条件分支逻辑而专门整理的一个 Map 对象(不是一个真正的 Map)。利用它可以免去很多 if 或者 switch 语句的判断分支,从而大大降低代码的复杂度。

import TYPES from './types'

// Object.prototype.toString() 输出的类型名称枚举值
const OBJECTS = {
  /* ===== Primitive data types ===== */
  '[object Null]': TYPES.NULL,
  /* ===== Keyed Collections ===== */
  '[object Set]': TYPES.SET,
  '[object WeakSet]': TYPES.WEAK_SET,
  '[object Map]': TYPES.MAP,
  '[object WeakMap]': TYPES.WEAK_MAP,
  /* ===== Array ===== */
  '[object Array]': TYPES.ARRAY,
  '[object Arguments]': TYPES.ARGUMENTS,
  /* ===== Typed Arrays ===== */
  '[object DataView]': TYPES.DATA_VIEW,
  '[object ArrayBuffer]': TYPES.ARRAY_BUFFER,
  '[object Int8Array]': TYPES.INT8_ARRAY,
  '[object Uint8Array]': TYPES.UNIT8_ARRAY,
  '[object Uint8ClampedArray]': TYPES.UNIT8_CLAMPED_ARRAY,
  '[object Int16Array]': TYPES.INT16_ARRAY,
  '[object Uint16Array]': TYPES.UNIT16_ARRAY,
  '[object Int32Array]': TYPES.INT32_ARRAY,
  '[object Uint32Array]': TYPES.UNIT32_ARRAY,
  '[object Float32Array]': TYPES.FLOAT32_ARRAY,
  '[object Float64Array]': TYPES.FLOAT64_ARRAY,
  '[object BigInt64Array]': TYPES.BIG_INT64_ARRAY,
  '[object BigUint64Array]': TYPES.BIG_UINT64_ARRAY,
  /* ===== Object ===== */
  '[object Object]': TYPES.OBJECT,
  '[object Boolean]': TYPES.OBJECT,
  '[object String]': TYPES.OBJECT,
  '[object Number]': TYPES.OBJECT,
  '[object Date]': TYPES.DATE,
  '[object Error]': TYPES.ERROR,
  '[object DocumentFragment]': TYPES.FRAGMENT,
  '[object Function]': TYPES.FUNCTION,
  '[object NodeList]': TYPES.COLLECTION,
  '[object Promise]': TYPES.PROMISE,
  '[object RegExp]': TYPES.REGEXP,
  '[object Text]': TYPES.TEXT
}

export default OBJECTS

_typeof() 方法实现

从 TYPES 对象我们已得知,_type() 方法并没有将 Standard built-in objects 中所有的对象类型都包含在内,而是根据实际的开发经验,将常用的数据类型基本都纳入进去了,代码如下:。

// enum/types.js 中存储的是最终显示的数据类型
import TYPES from './enum/types'
// enum/objects.js 中存储的则是使用 Object.prototype.toString() 得到对象类型的字符串
import OBJECTS from './enum/objects'

/**
 * 检测数据类型,返回检测数据类型的字符串
 * ========================================================================
 * @method _type
 * @param {*} val - 要检测的任意值
 * @returns {String}
 */
const _type = (val) => {
  const type = Object.prototype.toString.apply(val)
  const _typeof = typeof val
  let name

  // HTMLElement
  if (val?.tagName && val.nodeType === 1) {
    name = TYPES.ELEMENT
  } else {
    /* ===== 原始值类型(Primitive data types) ===== */
    switch(_typeof){
      case 'bigint':
        name = TYPES.BIG_INT
        break
      case 'string':
        name = TYPES.STRING
        break
      case 'number':
        name = TYPES.NUMBER
        break
      case 'boolean':
        name = TYPES.BOOLEAN
        break
      case 'undefined':
        name = TYPES.UNDEFINED
        break
      case 'symbol':
        name = TYPES.SYMBOL
        break
      // 对象(引用)类型的数据
      default:
        name = OBJECTS[type]
        break
    }
  }
  
  // 如果出现 OBJECTS 中没有包含的枚举值,
  // 则直接输出 '[object xxx]' 格式的类型字符串
  return name || type
}

export default _type

可以看到,除了内置对象,针对 DOM 操作的常用对象 HTMLElement、NodeList、TextNode 和 DocuemntFragement 对象,_type() 方法也是可以识别的。_type() 方法已经足够好了,但如果需你要更加完善的数据类型检测的 JavaScript 工具库,不妨试试我的 types.js 项目。