先看一段输出,你就知道数据类型判断为什么总被人反复问:
typeof null // "object" —— 明明是空值
typeof [] // "object" —— 分不清数组
typeof NaN // "number" —— 它压根不是数字
typeof function(){} // "function" —— 唯一被特殊对待的引用类型
new Date() instanceof Date // true
new Date() instanceof Object // true,链上有 Object
一句话概括:JavaScript 的类型系统里有几个历史遗留的坑,不同判断手段各管一段,没有一个是万能的。这篇文章把类型先分清楚,再把 typeof、instanceof、constructor、Object.prototype.toString 逐一对号入座。
一、JS 到底有几种数据类型
ECMAScript 语言层面把类型分成两大类:原始类型(Primitive) 和 引用类型(Object)。
原始类型一共 7 种,数据直接存在栈里,按值访问:
| 类型 | 说明 | 例子 |
|---|---|---|
Undefined | 声明未赋值 | let a |
Null | 空引用,历史遗留 | null |
Boolean | 布尔 | true / false |
Number | 双精度浮点 | 1 / 0.1 / NaN |
String | 字符串 | "abc" |
Symbol | 唯一标识符,ES6 | Symbol("id") |
BigInt | 任意精度整数,ES2020 | 10n |
引用类型则是一大类,统称 Object,常见的有:
- 普通对象
{} - 数组
[] - 函数
function(){} - 日期
new Date() - 正则
/abc/ - 以及
Map、Set、WeakMap、Promise等等
两者最本质的区别在复制行为:
// 原始类型:按值传递,互不影响
let a = 1
let b = a
b = 2
console.log(a) // 1
// 引用类型:变量存的是地址,改的是同一块内存
let o1 = { count: 1 }
let o2 = o1
o2.count = 2
console.log(o1.count) // 2
二、typeof:最常用,但有三个坑
typeof 是最顺手的运算符,返回一个字符串。它对原始类型几乎都能识别:
typeof undefined // "undefined"
typeof null // "object" ❌ 坑 1
typeof true // "boolean"
typeof 1 // "number"
typeof "abc" // "string"
typeof Symbol() // "symbol"
typeof 10n // "bigint"
对引用类型,它只能区分出函数,其余一律是 "object":
typeof {} // "object"
typeof [] // "object" ❌ 坑 2:数组识别不了
typeof null // "object"
typeof function(){} // "function"
另外 typeof 对未声明的变量不会抛错,这是它一个隐藏优点:
typeof notDefined // "undefined",不会报 ReferenceError
三个坑总结:
typeof null === "object":这是第一版 JS 用低位标记类型留下的 bug,值全 0 被视为对象指针,修复会破坏太多旧代码,于是被保留至今。typeof [] === "object":数组、日期、正则都无法区分。typeof NaN === "number":NaN全称 Not a Number,但它确实是 Number 类型的一个值。
所以 typeof 的正确用法是:判断原始类型和函数,判断 null 要单独写 value === null。
三、instanceof:靠原型链,只适合引用类型
instanceof 检查构造函数的 prototype 是否出现在对象的原型链上:
[] instanceof Array // true
[] instanceof Object // true,Array 继承自 Object
new Date() instanceof Date // true
new Date() instanceof Object // true
它能很好地解决数组、日期、正则的区分问题:
const isArray = (v) => v instanceof Array
const isDate = (v) => v instanceof Date
但有两个明显限制:
- 对原始类型几乎失效,因为原始值不是对象,没有原型链:
1 instanceof Number // false
"abc" instanceof String // false
- 跨 iframe / 跨 realm 会失灵,不同全局环境有各自的构造函数:
// 页面 A 的数组拿到页面 B 里判断
iframeArray instanceof Array // 可能为 false
还有一个要小心的点:instanceof 可以被 Symbol.hasInstance 自定义,也能通过改原型欺骗:
const fake = {}
Object.setPrototypeOf(fake, Array.prototype)
fake instanceof Array // true,但 fake 不是数组
四、constructor:能读,但不可靠
每个对象通过原型链能拿到 constructor 属性,指向创建它的构造函数:
(1).constructor === Number // true
"abc".constructor === String // true
[].constructor === Array // true
({}).constructor === Object // true
看起来不错,但它有两个致命问题:
// 1. null 和 undefined 没有 constructor,直接报错
null.constructor // TypeError
// 2. constructor 可以被随意改写
function Foo() {}
Foo.prototype = {} // 原型被替换,constructor 丢了
new Foo().constructor === Foo // false
所以 constructor 只能作为辅助,不能当作类型判断的主手段。
五、Object.prototype.toString:最通用的一招
这是社区公认最稳的判断方式。调用它时,内部会读取对象的 [[Class]] / Symbol.toStringTag,返回 "[object Xxx]":
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call("abc") // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(Symbol()) // "[object Symbol]"
Object.prototype.toString.call(10n) // "[object BigInt]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(new Date()) // "[object Date]"
Object.prototype.toString.call(/abc/) // "[object RegExp]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(new Map()) // "[object Map]"
它几乎能区分所有内置类型,而且不受原型链篡改影响。缺点是两个:
- 需要
.call()/.apply(),因为直接obj.toString()可能被对象自己重写。 - 对自定义类的实例,返回的还是
"[object Object]",无法区分业务对象。
六、几种针对性的快捷方法
有些场景有更语义化的 API,优先用它们:
Array.isArray([]) // true,官方推荐的数组判断
Number.isNaN(NaN) // true,只对真正的 NaN 返回 true
Number.isInteger(1.0) // true,判断整数
Object.is(NaN, NaN) // true,比 === 更严格(能区分 +0 / -0)
Array.isArray 能正确处理跨 realm 的数组,比 instanceof Array 更可靠。
七、手写一个通用的 type 函数
把上面几招组合起来,封装一个覆盖 99% 场景的工具:
function getType(value) {
// 优先用 toString 拿到内置类型
const raw = Object.prototype.toString.call(value)
const type = raw.slice(8, -1).toLowerCase() // "[object Array]" -> "array"
// 修正 null 和 NaN 这两个特殊值
if (value === null) return 'null'
if (type === 'number' && Number.isNaN(value)) return 'nan'
return type
}
getType(undefined) // "undefined"
getType(null) // "null"
getType(1) // "number"
getType(NaN) // "nan"
getType("abc") // "string"
getType(true) // "boolean"
getType(Symbol()) // "symbol"
getType(10n) // "bigint"
getType([]) // "array"
getType({}) // "object"
getType(new Date()) // "date"
getType(/abc/) // "regexp"
getType(function(){}) // "function"
如果还想支持 Map、Set、Promise,直接在上面结果上判断即可,toString 已经返回了对应的标签。
八、一张表选对工具
| 判断目标 | 推荐方式 | 备注 |
|---|---|---|
| 原始类型 | typeof v | null 例外,需 v === null |
| 函数 | typeof v === "function" | 简单直接 |
| 数组 | Array.isArray(v) | 支持跨 realm |
| 日期 / 正则 / Map / Set | Object.prototype.toString.call(v) | 最通用 |
NaN | Number.isNaN(v) | 不要用全局 isNaN |
| 整数 | Number.isInteger(v) | — |
| 是某个类的实例 | v instanceof Foo | 仅引用类型,注意跨 realm |
| 自定义对象 | 自定义 Symbol.toStringTag | toString 会读取它 |
小结
- JS 有 7 种原始类型和一类引用类型,复制行为完全不同。
typeof判原始类型和函数,但null会误报"object",数组分不出。instanceof基于原型链,只适合引用类型,跨 iframe 会失效。constructor可被改写,null上会报错,别单独用。Object.prototype.toString.call()是通用解法,配合特殊值修正可封装出稳定的getType。- 日常优先用针对性 API:
Array.isArray、Number.isNaN、Number.isInteger。
记住一句话:没有万能钥匙,先想清楚你要判断的是哪一层——原始值、内置对象,还是某个自定义类的实例,再选对应的工具。