导读
在 JavaScript 中,数据类型是一个非常重要的概念,因为它决定了变量可以存储什么样的数据以及可以对这些数据执行什么操作。了解如何判断数据的类型对于编写健壮的 JavaScript 代码至关重要。
而 JavaScript 发展至今,一直保持着弱类型(weakly typed)语言的特性,也就一直没有提供准确的检测数据类型的内置方法。因此开发者就不得不自己编写校验数据类型的函数方法。在本文中,我们将探讨 JavaScript 中各种判断数据类型的方法。
JavaScript 的数据类型
根据 MDN 的《JavaScript data types and data structures》这篇文档介绍,JavaScript 将数据分为了以下几个大类:
原始数据类型(Primitive Types)
其中原始值是是我们最常用的“数据”,JavaScript 有7个原始值:
引用数据类型(Reference Types)
object(对象类型),包括普通对象、数组、函数等。
利用 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'(对象),而作为开发者是希望知道这些“对象”具体是什么“对象”的。
引用数据类型(Reference Types)的细分
通过 typeof 关键字判断对象类型的值,得到的结果都一样。但实际上引用数据(对象Object)类型的数据也是有更细的类别分的:
完整的所有 JavaScript 内置对象请参考 Standard built-in objects。
索引集合(Indexed Collections)
在 JavaScript 中,索引集合(Indexed Collections)是指那些基于索引访问元素的数据结构。这些集合允许我们通过索引(通常是数字)来访问和操作其中的元素。在 JavaScript 中,主要有两种索引集合:数组(Array)和类型化数组(Typed Arrays) 。
数组(Array)
数组是 JavaScript 中最常用的索引集合。它是一个可调整大小的、基于索引的集合,可以存储任意类型的元素(包括其他数组)。数组中的元素按索引顺序存储,索引从 0 开始。
类型化数组(Typed Arrays)
类型化数组(Typed Arrays)是一个用来处理原始二进制数据的数组,通常用于处理 WebGL 和其他需要高性能操作的场景。类型化数组可以看作是一个描述原始二进制缓冲区(ArrayBuffer)的数组视图。每种类型化数组都有一个固定的、特定的数据类型和大小。
- Int8Array:8 位有符号整数,每个元素 1 字节
- Uint8Array:8 位无符号整数,每个元素 1 字节
- Uint8ClampedArray:8 位无符号整数(溢出截断),每个元素 1 字节
- Int16Array:16 位有符号整数,每个元素 2 字节
- Uint16Array:16 位无符号整数,每个元素 2 字节
- Int32Array:32 位有符号整数,每个元素 4 字节
- Uint32Array:32 位无符号整数,每个元素 4 字节
- Float32Array:32 位 IEEE 浮点数,每个元素 4 字节
- Float64Array:64 位 IEEE 浮点数,每个元素 8 字节
键控集合(Keyed Collections)
在 JavaScript 中,键控集合(Keyed Collections)是指允许以键值对(key-value pair)形式存储和操作数据的集合。键控集合提供了一种更灵活的方式来存储数据,而不仅仅是基于索引。JavaScript 提供了两种主要的键控集合类型:Map 和 Set,以及它们的衍生类 WeakMap 和 WeakSet。
Map
Map 是 JavaScript 中的一种键控集合,允许我们将键映射到值。Map 中的键和值可以是任意类型,包括对象和原始数据类型。
Set
Set 是 JavaScript 中的另一种键控集合,表示一组唯一的值,不能包含重复的值。Set 允许我们存储任意类型的值,无论是原始值还是对象引用。
WeakMap
WeakMap 是一种特殊的 Map,其中的键必须是对象,且这些对象是弱引用的,这意味着如果没有其他引用指向这些对象,垃圾回收机制会自动回收这些对象。
WeakSet
WeakSet 是一种特殊的 Set,其中的值必须是对象,且这些对象是弱引用的。这种集合类似于 WeakMap,但存储的是一组不重复的对象。
结构化数据(Structured Data)
在 JavaScript 中,结构化数据(Structured Data)是指那些按特定结构组织的数据,以便于数据的访问和处理。结构化数据通常采用对象、数组以及一些内置的对象(如 JSON)的形式。通过这些数据结构,我们可以存储和操作复杂的数据集合。
ArrayBuffer`
ArrayBuffer 是一种通用的、固定长度的二进制数据缓冲区,类型化数组和 DataView 视图都基于 ArrayBuffer 实现。
DataView
DataView 允许对 ArrayBuffer 进行低级别的、无类型的数据操作,支持多种数据类型。
SharedArrayBuffer
SharedArrayBuffer 是 JavaScript 中一种允许多线程共享内存的对象。它与普通的 ArrayBuffer 类似,但有一个关键的区别:SharedArrayBuffer 可以在多个线程(如主线程和 Web Workers)之间共享,这使得线程之间可以高效地共享数据而不需要将数据复制到每个线程的内存空间中。
SharedArrayBuffer 主要用于构建高级的并发编程结构,如通过 Atomics 实现的低级同步操作。
Atomics
Atomics 是 JavaScript 中用于在多线程环境下进行低级同步的对象。它提供了一组静态方法,用于在共享内存中的 SharedArrayBuffer 上执行原子操作(atomic operations)。这些操作在多线程下是安全的,因为它们可以防止数据竞争和竞争条件。
Atomics 对象不能被构造或实例化,它提供了一些方法来确保在多线程环境中进行无锁操作时数据的一致性。
JSON
JSON 是一种轻量级的数据交换格式,易于人们编写和读取,也易于机器解析和生成。JavaScript 原生支持 JSON,可以将对象或数组转换为 JSON 格式字符串,也可以将 JSON 字符串解析为 JavaScript 对象。
基本对象(Fundamental Objects)
在 JavaScript 中,基本对象(Fundamental Objects)是提供核心功能的内建对象,这些对象在 JavaScript 中用于表示简单的数据类型和常用的基础操作。它们为开发者提供了对 JavaScript 语言核心的低级访问,是构建更复杂对象和数据结构的基础。
以下是 JavaScript 中的基本对象:
Object
Object 是所有 JavaScript 对象的基类,所有对象都继承自 Object。它提供了许多实用的方法,用于操作对象的属性和原型链。
Function
Function 是 JavaScript 中的一个特殊对象,它可以被调用。函数是 JavaScript 的一等对象,可以赋值给变量、作为参数传递、返回等等。
Boolean
Boolean 对象是 JavaScript 中的基本数据类型之一,用于表示两个值之一:true 或 false。
Symbol
Symbol 是 JavaScript 中的一种基本数据类型,用于创建独一无二的标识符。它最常用于定义对象的唯一属性名,防止属性名冲突。
除了上面介绍的引用数据类型数据。还有 Dates 和 Error objects 类型:
引用数据类型数据的判断
在了解引用数据(对象)类型后,现在就需要想办法来判断这些具体的对象数据类型了。
使用 Object.prototype.toString.call()
对于复杂的引用数据类型判断,尤其是对 null 和其他内置对象(如 Date、RegExp、Error)的判断,Object.prototype.toString.call() 是一种通用方法。它可以准确地返回对象的内部 [[Class]] 属性。
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]'
// ... 省略其它类型数据的判断
看来对于引用数据类型数据的判断使用 Object.prototype.toString.call() 来判断,问题是几乎已经得到了解决,是不是?
封装 types() 方法
根据之前的了解,可以使用 typeof 关键字判断原始值类型的数据(null除外),使用 Object.prototype.toString.call() 方法判断对象类型的值,因此我们可以整理一个 types() 方法:
类型名称枚举
// 能够识别的数据类型名称枚举值
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
对象名称枚举
import TYPES from './types'
// Object.prototype.toString.call() 输出的类型名称枚举值
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
types() 方法
// enum/types.js 中存储的是最终显示的数据类型
import TYPES from './enum/types'
// enum/objects.js 中存储的则是使用 Object.prototype.toString() 得到对象类型的字符串
import OBJECTS from './enum/objects'
/**
* 检测数据类型,返回检测数据类型的字符串
* ========================================================================
* @method types
* @param {*} val - 要检测的任意值
* @returns {String}
*/
const types = (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 types
types() 方法并没有将 Standard built-in objects 中所有的对象类型都包含在内,而是根据实际的开发经验,将常用的数据类型基本都纳入进去了,当然大家也可以根据自己的需求加入其它需要检测的数据类型。
另外,除了内置对象,针对 DOM 操作的常用对象 HTMLElement、NodeList、TextNode 和 DocuemntFragement 对象,types() 方法也是可以识别的。
JavaScript 中提供的其它类型判断的方法
虽然我们已经封装了 types() 方法,但这里我还要介绍一下 JavaScript 中提供的其它类型判断的方法:
instanceof 操作符
instanceof 用于检测一个对象是否是某个构造函数的实例,适用于引用类型。
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
function CustomClass() {}
const instance = new CustomClass();
console.log(instance instanceof CustomClass); // true
Array.isArray()
判断一个值是否是数组,使用 Array.isArray() 方法。这是一个专门用于判断数组的方法,比使用 instanceof Array 更可靠,因为它还可以处理不同 JavaScript 执行环境之间(如 iframe)的数组判断。
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
constructor 属性
在一些情况下,你可以使用对象的 constructor 属性来检查它的类型。这个方法依赖于对象的 constructor 属性,但需要注意,如果对象的 constructor 被篡改过,这种方法就不可靠了。
console.log(({}).constructor === Object); // true
console.log(([]).constructor === Array); // true
console.log((new Date()).constructor === Date); // true
我的另外两篇文章《JavaScript 如何判断 prototype 对象?》和 《JavaScript 如何判断函数为构造函数?》就是借助 Function 对象的 constructor 属性和 prototype 对象的constructor 属性进行的类型检测。
总结
希望大家通过阅读本文能够更进一步的了解 JavaScript 的常见的数据类型,以及 typeof 操作符到更复杂的 Object.prototype.toString.call() 方法适用的类型检测的应用场景。
就像文本开始介绍的,了解如何判断数据的类型对于编写健壮的 JavaScript 代码至关重要。可以帮助我们编写更加健壮和可靠的 JavaScript 代码。
(PS: 如果希望更加完善的数据类型检测库,了解更多全面的数据类型的检测方法,可以查看我的 types.js 项目)