JS 判断数据类型的 8 种方式
Category(分类): JavaScript Status: 已整理(2026)
本文保留原文的八类方法:
typeof、constructor、instanceof、原型查询、Object.prototype.toString、鸭子类型、Symbol.toStringTag和直接比较,并修复抓取导致的代码粘连。新增bigint、TDZ、跨 Realm、Proxy、Symbol.hasInstance、Object.is和现代推荐选择。
一、先区分“类型判断”的目标
类型判断可能是在回答不同问题:
- 它是不是某一种原始类型?
- 它是不是数组、日期或正则?
- 它是否由某个 class 创建,或原型链上是否存在某个原型?
- 它是否具备某种能力,例如
then方法? - 它是否是
NaN、有限数字、null或undefined? - 它是否来自另一个 iframe/Realm?
没有一个 API 能正确回答所有问题。应先明确目标,再选择工具。
ECMAScript 的原始类型包括 undefined、null、boolean、string、symbol、number、bigint;对象是另一类语言类型。typeof 返回的是字符串标签,不是完整的类型系统。
二、typeof
typeof 适合快速判断大多数原始类型和“是否为函数”:
console.log(typeof undefined) // 'undefined'
console.log(typeof null) // 'object':历史兼容行为
console.log(typeof true) // 'boolean'
console.log(typeof 123) // 'number'
console.log(typeof NaN) // 'number'
console.log(typeof 123n) // 'bigint'
console.log(typeof 'hello') // 'string'
console.log(typeof Symbol('id')) // 'symbol'
console.log(typeof {}) // 'object'
console.log(typeof []) // 'object'
console.log(typeof function () {}) // 'function'
2.1 typeof null
typeof null === 'object' 是早期 JavaScript 的历史兼容结果,不能据此把 null 当作普通对象:
function isNull(value) {
return value === null
}
console.log(isNull(null)) // true
console.log(isNull({})) // false
2.2 typeof document.all
在浏览器中,document.all 是一个特殊的 Web 兼容对象,typeof document.all 可能返回 'undefined',同时它还具有历史上的特殊真假值行为。它不是普通 ECMAScript 对象,也不能作为判断一般对象的依据:
if (typeof document !== 'undefined') {
console.log(typeof document.all) // 浏览器中的兼容特例
}
2.3 TDZ 中的 typeof 也可能抛错
typeof 对未声明的标识符通常不会抛错,但处于暂时性死区(TDZ)的 let、const 或 class 绑定会抛出 ReferenceError:
try {
console.log(typeof temporalValue)
let temporalValue = 1
} catch (error) {
console.log(error.name) // ReferenceError
}
因此不能用 typeof 绕过所有未初始化变量错误。
三、constructor
对象通常可以沿原型链访问 constructor:
const array = [1, 2, 3]
const object = { name: 'Ada' }
console.log(array.constructor === Array) // true
console.log(object.constructor === Object) // true
但 constructor 不是可靠的类型品牌:
null和undefined没有可访问的constructor;- 属性可以被实例自身覆盖;
- 原型对象可以被替换或篡改;
Object.create(null)没有Object.prototype;- 不同 Realm 中的
Array、Object构造器不是同一个函数。
const user = {}
user.constructor = 'not a constructor'
console.log(user.constructor) // not a constructor
console.log(user instanceof Object) // true
如果只是想获取对象的原型,使用 Object.getPrototypeOf:
console.log(Object.getPrototypeOf(array) === Array.prototype) // true
console.log(Object.getPrototypeOf(object) === Object.prototype) // true
__proto__ 是 legacy accessor,不建议作为现代代码的首选。
四、instanceof
基本形式:
const array = [1, 2, 3]
const object = { name: 'Ada' }
console.log(array instanceof Array) // true
console.log(array instanceof Object) // true
console.log(object instanceof Object) // true
console.log(object instanceof Array) // false
它主要检查:右操作数的 prototype 是否出现在左操作数的原型链上。它不是“这个值曾由哪个构造函数创建”的绝对证明。
4.1 右侧不一定写成 class
右侧必须提供合法的 @@hasInstance 行为。class 和普通函数通常可以;拥有可调用 Symbol.hasInstance 的对象也可以:
const evenNumber = {
[Symbol.hasInstance](value) {
return typeof value === 'number' && value % 2 === 0
}
}
console.log(2 instanceof evenNumber) // true
console.log(3 instanceof evenNumber) // false
因此,“右侧必须是函数或者 class”是过于简化的说法。
4.2 跨 Realm 限制
不同 iframe、Worker 或 VM Realm 中各自拥有不同的内建构造器:
// 浏览器中:otherWindow.Array !== window.Array
// otherWindowArray instanceof Array 可能为 false
跨 Realm 判断数组应使用 Array.isArray,不要使用 value instanceof Array。
4.3 原型被修改和 Proxy
instanceof 可能受原型修改、Proxy 和自定义 Symbol.hasInstance 影响;它适合可信代码、同一 Realm 和稳定原型的场景,不适合安全品牌校验。
五、手写 instanceof(仅用于理解)
下面的实现演示原型链遍历,但不等价于原生 instanceof:
function myInstanceof(value, Constructor) {
if (typeof Constructor !== 'function') {
throw new TypeError('这个教学版本的右侧必须是函数')
}
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
return false
}
const prototype = Constructor.prototype
if (typeof prototype !== 'object' || prototype === null) {
throw new TypeError('Constructor.prototype 必须是对象')
}
let current = Object.getPrototypeOf(value)
while (current !== null) {
if (current === prototype) return true
current = Object.getPrototypeOf(current)
}
return false
}
function User() {}
const user = new User()
console.log(myInstanceof(user, User)) // true
console.log(myInstanceof({}, User)) // false
这个教学版本没有实现:
Symbol.hasInstance;- 右侧 Proxy 的行为;
- 跨 Realm 特殊情况;
- 原生算法的全部异常边界。
生产代码应直接使用 instanceof,不要为了“手写面试题”替换原生语义。
六、原型查询:isPrototypeOf 与 Object.getPrototypeOf
isPrototypeOf 判断一个对象是否出现在另一个对象的原型链上:
const object = { name: 'Ada' }
const array = [1, 2, 3]
console.log(Object.prototype.isPrototypeOf(object)) // true
console.log(Array.prototype.isPrototypeOf(array)) // true
console.log(Array.prototype.isPrototypeOf(object)) // false
正确拼写是 isPrototypeOf,不是 isPrototypeof。它的接收者必须是对象,但传入的待检查值为 null 或 undefined 时会返回 false:
console.log(Object.prototype.isPrototypeOf(null)) // false
console.log(Object.prototype.isPrototypeOf(undefined)) // false
与之不同,Object.getPrototypeOf(null) 和 Object.getPrototypeOf(undefined) 才会抛出 TypeError。
Object.getPrototypeOf 返回对象的直接原型:
console.log(Object.getPrototypeOf(object) === Object.prototype) // true
console.log(Object.getPrototypeOf(array) === Array.prototype) // true
console.log(Object.getPrototypeOf(Object.create(null)) === null) // true
这两个 API 都是原型关系查询,并不等同于完整的 instanceof:instanceof 还可能调用右侧的 Symbol.hasInstance。
七、一些针对性标准 API
7.1 Array.isArray
判断数组应优先使用 Array.isArray:
console.log(Array.isArray([1, 2, 3])) // true
console.log(Array.isArray({})) // false
console.log(Array.isArray('array')) // false
它比 instanceof Array 更适合跨 Realm 的数组判断。
7.2 Number.isNaN 与 Number.isFinite
console.log(Number.isNaN(NaN)) // true
console.log(Number.isNaN('NaN')) // false
console.log(Number.isNaN({})) // false
console.log(Number.isFinite(123)) // true
console.log(Number.isFinite(Infinity)) // false
console.log(Number.isFinite('123')) // false:不会隐式转换
7.3 Object.hasOwn
如果目标是判断对象是否拥有自有属性,不要用类型判断 API:
const object = Object.create({ inherited: true })
object.own = true
console.log(Object.hasOwn(object, 'own')) // true
console.log(Object.hasOwn(object, 'inherited')) // false
console.log('inherited' in object) // true:包含原型链
八、Object.prototype.toString
借用 Object.prototype.toString 可以得到许多内建对象的标签:
const toString = Object.prototype.toString
console.log(toString.call(123)) // [object Number]
console.log(toString.call(123n)) // [object BigInt]
console.log(toString.call('hello')) // [object String]
console.log(toString.call(true)) // [object Boolean]
console.log(toString.call(undefined)) // [object Undefined]
console.log(toString.call(null)) // [object Null]
console.log(toString.call({})) // [object Object]
console.log(toString.call([])) // [object Array]
console.log(toString.call(function () {})) // [object Function]
console.log(toString.call(new Date())) // [object Date]
console.log(toString.call(/pattern/)) // [object RegExp]
可以封装成辅助函数:
function getTag(value) {
return Object.prototype.toString.call(value).slice(8, -1)
}
console.log(getTag(Symbol('id'))) // Symbol
console.log(getTag(new Map())) // Map
console.log(getTag(new Set())) // Set
8.1 Symbol.toStringTag 可改变标签
const custom = {
get [Symbol.toStringTag]() {
return 'CustomValue'
}
}
console.log(Object.prototype.toString.call(custom)) // [object CustomValue]
标签是可观察的显示信息,不是防伪品牌。读取标签可能触发 getter;在 Proxy 上还可能执行用户代码。因此 Object.prototype.toString 适合作为辅助诊断,不应作为安全边界或绝对类型证明。
九、鸭子类型(Duck Typing)
鸭子类型不是判断“它来自哪个构造器”,而是判断对象是否具备需要的能力:“如果它走起来像鸭子、叫起来像鸭子,就可以按鸭子使用”。
function canBeClosed(resource) {
return resource !== null && typeof resource?.close === 'function'
}
function useResource(resource) {
if (!canBeClosed(resource)) {
throw new TypeError('resource 必须提供 close()')
}
try {
return resource.read?.()
} finally {
resource.close()
}
}
鸭子类型适合能力型 API、插件和跨 Realm thenable,但不要仅凭一个容易伪造的方法就授予敏感权限。
9.1 Promise/thenable 判断
value instanceof Promise 不能可靠识别跨 Realm Promise,也不能覆盖所有 thenable。若只需要等待一个值,直接使用 Promise.resolve(value):
function isThenable(value) {
return value !== null &&
(typeof value === 'object' || typeof value === 'function') &&
typeof value.then === 'function'
}
console.log(isThenable(Promise.resolve())) // true
console.log(isThenable({ then() {} })) // true
console.log(isThenable({ catch() {} })) // false:catch 不是 thenable 的必要条件
“是否是原生 Promise”与“是否可被 Promise 处理”是不同问题,应根据 API 契约选择判断方式。
十、Symbol.toStringTag
Symbol.toStringTag 允许对象为 Object.prototype.toString 提供自定义标签:
class MyCollection {
get [Symbol.toStringTag]() {
return 'MyCollection'
}
}
const collection = new MyCollection()
console.log(Object.prototype.toString.call(collection)) // [object MyCollection]
它适合调试、日志和开发者工具展示,不适合做安全类型校验。若需要识别自己定义的 class,可以使用私有字段品牌:
class Token {
#brand = true
static isToken(value) {
try {
return value instanceof Token && value.#brand === true
} catch {
return false
}
}
}
console.log(Token.isToken(new Token())) // true
私有字段不能被普通对象伪造,但跨 Realm 的对象仍需由跨 Realm 的协议或消息边界重新验证。
十一、直接比较与特殊值
有些“类型判断”其实只是判断特殊值:
function isNull(value) {
return value === null
}
function isUndefined(value) {
return value === undefined
}
function isBoolean(value) {
return value === true || value === false
}
console.log(isNull(null)) // true
console.log(isUndefined(undefined)) // true
console.log(isBoolean(false)) // true
Object.is 可以处理 NaN 和 -0 的特殊比较:
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
console.log(0 === -0) // true
console.log(Object.is(0, -0)) // false
原文提到 underscore 的 isNull、isUndefined、isBoolean 等工具函数,它们的核心仍然是严格比较或标准 API。现代项目不需要为了 void 0 专门封装 undefined 判断。
11.1 void 0 的历史背景
console.log(void 0 === undefined) // true
早期浏览器中,顶层 undefined 可能被覆盖,因此代码曾使用 void 0 获得稳定的 undefined 值。现代 JavaScript 中 undefined 已是不可重新绑定的全局属性,普通业务代码直接使用 value === undefined 即可;void 0 可以作为历史兼容写法保留,但不再是必需方案。
十二、NaN 判断
12.1 全局 isNaN
全局 isNaN 会先进行数值转换:
console.log(isNaN(NaN)) // true
console.log(isNaN({})) // true:对象转换成 NaN
console.log(isNaN('123')) // false:字符串转换成 123
这在需要“转换后是否不是数字”时可能有用,但不适合严格判断输入是否是 NaN。
12.2 Number.isNaN
console.log(Number.isNaN(NaN)) // true
console.log(Number.isNaN({})) // false
console.log(Number.isNaN('NaN')) // false
推荐使用 Number.isNaN。如果必须支持非常旧的环境,可以使用:
function isNaNValue(value) {
return typeof value === 'number' && value !== value
}
value !== value 只有 NaN 满足,但可读性通常不如 Number.isNaN。
十三、indexOf 与 includes
数组的 indexOf 使用严格相等,找不到 NaN;includes 使用 SameValueZero,可以找到 NaN:
const values = [NaN]
console.log(values.indexOf(NaN)) // -1
console.log(values.includes(NaN)) // true
查找对象时二者都按引用比较,不会根据对象内容深度比较:
console.log([{ id: 1 }].includes({ id: 1 })) // false
十四、常用选择表
| 目标 | 推荐方法 | 主要边界 |
|---|---|---|
| 判断原始类型 | typeof | null 为 object,NaN 为 number,包含 bigint/symbol |
判断 null | value === null | null 是独立的原始值 |
| 判断数组 | Array.isArray(value) | 比 instanceof Array 更适合跨 Realm |
| 判断 NaN | Number.isNaN(value) | 不做隐式转换 |
| 判断有限数字 | Number.isFinite(value) | 不做隐式转换 |
| 判断同 Realm class 实例 | value instanceof Class | 受原型、Proxy、Realm、hasInstance 影响 |
| 判断原型关系 | Object.getPrototypeOf / isPrototypeOf | 查询的是原型链,不是品牌 |
| 获取内建标签 | Object.prototype.toString.call(value) | Symbol.toStringTag 可改写,可能执行 getter |
| 判断能力 | 显式检查方法/属性 | 能力可能被伪造或有副作用 |
| 判断自有属性 | Object.hasOwn(value, key) | 不要用 in 替代 |
判断 -0、NaN | Object.is | 与 === 的零和 NaN 规则不同 |
十五、一个组合式类型辅助函数
如果项目需要统一输出类型标签,可以把规则写成有限且可审计的函数,不要宣称它能识别所有对象品牌:
function describeValue(value) {
if (value === null) return 'null'
const primitiveType = typeof value
if (primitiveType !== 'object') return primitiveType
if (Array.isArray(value)) return 'array'
const tag = Object.prototype.toString.call(value)
switch (tag) {
case '[object Date]':
return 'date'
case '[object RegExp]':
return 'regexp'
case '[object Map]':
return 'map'
case '[object Set]':
return 'set'
default:
return 'object'
}
}
console.log(describeValue(null)) // null
console.log(describeValue(1n)) // bigint
console.log(describeValue([])) // array
console.log(describeValue(new Date())) // date
这个辅助函数仍会受到 Proxy、Symbol.toStringTag 和跨 Realm 细节影响;它适合日志和业务分支,不应作为安全验证的唯一依据。
十六、总结
typeof适合原始类型和函数的快速判断,但要记住null、NaN、bigint、symbol、TDZ 和document.all;constructor来自属性/原型查找,可以被覆盖,不适合作为可靠品牌;instanceof查询原型链,受到Symbol.hasInstance、Proxy、原型修改和跨 Realm 影响;isPrototypeOf应按正确大小写书写,Object.getPrototypeOf读取直接原型;- 数组使用
Array.isArray,NaN 使用Number.isNaN,有限数字使用Number.isFinite; Object.prototype.toString是有用的辅助标签,但Symbol.toStringTag可以改变显示结果;- 鸭子类型检查能力,不等于检查构造器身份;
Object.is适合需要区分 NaN 或-0的比较;- 判断自有属性使用
Object.hasOwn,包含原型链使用in; - 类型判断应服务于明确的业务契约,不能把某个字符串标签当作绝对安全证明。