JS中typeof能判断哪些类型?为什么null返回object
深入解析 JavaScript 中 typeof 运算符能判断的 8 种数据类型,图解剖析早期 C 语言底层中 32 位机器字与类型标签导致 typeof null 返回 object 的历史真相,并提供实用的精准类型判断最佳实践。
typeof 运算符一共可能返回 8 种字符串值:"undefined"、"boolean"、"number"、"bigint"、"string"、"symbol"、"function" 和 "object"。它能够精准区分 6 种基础原始类型以及函数,但无法区分数组、普通对象、日期等引用类型,并且会将原始类型 null 判定为 "object"。这一反直觉的表现源于 1995 年 JavaScript 第一版设计中的底层实现 Bug:在早期 32 位机器字结构中,000 标签位被用来标记对象指针,而 C 语言空指针 NULL 的机器码恰好全部为 0(末三位也是 000),导致引擎在分发类型时误入对象分支。
一、typeof 运算符能判断哪些类型?
typeof 是 JavaScript 中唯一的单目类型运算符,用于在运行时检测操作数的数据类型,并以小写字符串形式返回检测结果。截至 ECMAScript 最新规范,typeof 的返回值集合是极其固定的,总共仅有以下 8 种取值:
1. 六种基础原始类型的判断表现
除 null 外,typeof 能够完美且直观地识别 JavaScript 规范中的另外 6 种原始数据类型(Primitive Types):
- 未定义类型(Undefined):未赋值的变量或未声明的属性,返回
"undefined"。 - 布尔类型(Boolean):
true与false,返回"boolean"。 - 数字类型(Number):包含普通整数、浮点数、
+Infinity、-Infinity,以及特殊的非数字值NaN,均返回"number"。 - 大整数类型(BigInt):ES2020 引入的大整数(如
100n、BigInt("9007199254740993")),返回"bigint"。 - 字符串类型(String):单引号、双引号或模板字符串字面量,返回
"string"。 - 符号类型(Symbol):ES6 引入的唯一键标识符(如
Symbol("id")),返回"symbol"。
// 原始数据类型的 typeof 表现示例
console.log(typeof undefined); // "undefined"
console.log(typeof true); // "boolean"
console.log(typeof 42); // "number"
console.log(typeof NaN); // "number"(注意:NaN 是数字类型中的非正常值)
console.log(typeof 10n); // "bigint"
console.log(typeof "hello"); // "string"
console.log(typeof Symbol("key"));// "symbol"
2. 函数类型的特殊返回值:"function"
从 JavaScript 语言规范层面看,函数(Function)本质上属于可调用的引用对象(实现了内部的 [[Call]] 插槽)。然而,为了方便开发者在日常编码中快速区分可执行函数与普通数据结构,规范专门为函数开辟了独立的返回值:
function add(a, b) { return a + b; }
const multiply = (a, b) => a * b;
class User {}
console.log(typeof add); // "function"
console.log(typeof multiply); // "function"
console.log(typeof User); // "function"(类在底层也是构造函数)
console.log(typeof Math.sin); // "function"
3. 所有其他引用类型统称:"object"
当操作数是除了函数以外的任意引用对象时,typeof 均统一返回 "object"。这就意味着,typeof 无法直接区分普通键值对对象、数组、正则表达式或日期对象:
console.log(typeof { name: "Alice" }); // "object"
console.log(typeof [1, 2, 3]); // "object"(无法识别人性化的 "array")
console.log(typeof new Date()); // "object"
console.log(typeof /^[a-z]+$/); // "object"
console.log(typeof new Map()); // "object"
console.log(typeof new Set()); // "object"
4. typeof 的安全防御特性:未声明变量检测
在 JavaScript 中,直接读取一个尚未声明(未曾使用 var、let 或 const 声明)的变量会抛出致命的 ReferenceError 异常并中断代码执行。但 typeof 拥有特殊的防御保护机制:对未声明变量执行 typeof 不会报错,而是安全返回 "undefined"。
// 安全检测全局环境中是否存在某个第三方库或全局变量
if (typeof window !== "undefined" && typeof jQuery !== "undefined") {
console.log("jQuery 已加载");
}
防御保护仅适用于从未声明过的变量。如果变量在当前作用域内使用了 let 或 const 声明,但在声明语句执行前对其调用 typeof,仍然会触发暂时性死区(Temporal Dead Zone)错误,抛出 ReferenceError:
// 抛出异常:Cannot access 'version' before initialization
console.log(typeof version);
let version = "1.0.0";
二、为什么 typeof null 会返回 "object"?
在日常开发与初学者认知中,null 明明是 7 种原始类型之一,代表“空值”或“无对象指针”,但执行 typeof null 时输出的却是 "object":
console.log(typeof null); // "object"
这不是一种精心设计的语法特性,而是一个源自 1995 年的第一代引擎实现缺陷(历史 Bug)。为了彻底弄清其成因,我们需要回顾早期 JavaScript 引擎的底层内存模型。
1. 早期 SpiderMonkey 的 32 位机器字设计
1995 年,Brendan Eich 仅用了 10 天时间就完成了 JavaScript(代号 Mocha)的第一版原型。在由 C 语言编写的早期引擎(即后来演变为 SpiderMonkey 的代码库)中,出于内存效率与性能的权衡,JavaScript 中的所有值都使用一个统一的 32 位单元来表示(类型名为 jsval)。
在这 32 位机器字中,低位被划分为类型标签(Type Tag),高位则存放实际的数据载荷或内存指针。当时规定的类型标签位定义如下:
| 标签位(低 1~3 位) | 宏定义名称 | 对应 JavaScript 类型与高位含义 |
|---|---|---|
000 |
JSVAL_OBJECT |
对象引用,高 29 位存放对象在堆内存中的内存地址指针 |
1 |
JSVAL_INT |
小整数,高 31 位存放 31 位带符号整数字面量 |
010 |
JSVAL_DOUBLE |
双精度浮点数,高 29 位存放浮点数结构体指针 |
100 |
JSVAL_STRING |
字符串,高 29 位存放字符串结构体指针 |
110 |
JSVAL_BOOLEAN |
布尔值,高 29 位存放布尔字面值(0 或 1) |
2. C 语言空指针与类型分发的碰撞
在 C 语言的底层内存规范中,空指针 NULL 的数值定义就是整型 0(即 32 个二进制位全为 0,十六进制表示为 0x00000000)。
当 JavaScript 引擎实现 typeof 运算符时,底层的判定逻辑非常简单粗暴——直接通过位与操作提取该值的低 3 位标签值进行 switch-case 分发:
/* 早期 JavaScript 引擎源码伪代码剖析 */
JS_PUBLIC_API(JSType)
JS_TypeOfValue(JSContext *cx, jsval v)
{
JSType type = JSTYPE_VOID;
if (JSVAL_IS_VOID(v)) {
type = JSTYPE_VOID; // undefined
} else if (JSVAL_IS_OBJECT(v)) {
/*
* 关键宏定义:#define JSVAL_IS_OBJECT(v) (((v) & 0x7) == 0)
* 当 v 为 null 时,v 的机器值为 0x00000000。
* 计算 (0 & 0x7) 结果恰好为 0,完美命中该分支!
*/
JSObject *obj = JSVAL_TO_OBJECT(v);
if (obj && obj->map->clasp->call) {
return JSTYPE_FUNCTION; // 如果是可调用对象,返回 "function"
}
return JSTYPE_OBJECT; // 否则直接返回 "object"
} else if (JSVAL_IS_NUMBER(v)) {
type = JSTYPE_NUMBER;
} else if (JSVAL_IS_STRING(v)) {
type = JSTYPE_STRING;
} else if (JSVAL_IS_BOOLEAN(v)) {
type = JSTYPE_BOOLEAN;
}
return type;
}
由于 null 的机器码是 0x00000000,它的末尾 3 位自然也是 000。当引擎执行 JSVAL_IS_OBJECT(v) 宏检测时,条件立刻成立;接着判断该指针是否为可调用对象,由于空指针解引用校验不会进入 call 分支,最终直接退回返回了 JSTYPE_OBJECT(即字符串 "object")。
3. 为什么后来的 ECMAScript 规范不修复这个错误?
这个 Bug 在 JavaScript 发布后不久就被开发者广泛察觉。在 ECMAScript 6(ES6/Harmony)制定阶段,TC39 委员会曾经正式讨论过一个修正提案,试图将 typeof null 改为返回 "null"。
然而在对全网现有网页代码进行兼容性模拟测试后,该提案被断然否决。原因在于向后兼容性(Backward Compatibility)是 Web 标准的第一铁律:
- 早期的海量开源库、网页脚本中充斥着形如
if (typeof val === 'object')的逻辑,甚至有大量专门针对该 Bug 的补丁代码(如if (typeof val === 'object' && val !== null))。 - 一旦浏览器原生将
typeof null改为"null",那些使用typeof val === 'object'判断传参是否为“对象或空对象”的存量业务系统将全部出现逻辑崩塌,造成大面积网页瘫痪。
正如 Brendan Eich 本人后来在技术公开场合所言:“这个 Bug 已经成为了事实标准(de facto standard),修正它的代价远大于保留它的代价。”因此,在 ECMAScript 规范中,这一行为被作为永久性的语言规范特性保留了下来。
三、typeof 的局限性与常见陷阱
了解了 typeof 的底层机制后,我们可以清楚地看到它在实际工程中的能力边界。如果过度依赖 typeof,很容易踩中以下三个典型陷阱:
1. 无法区分细分引用类型
对于任何对象实例,typeof 都只能笼统给出 "object"。在面对以下数据结构时,它完全失去了区分能力:
typeof [] === "object"; // 数组无法被识别
typeof new Date() === "object"; // 日期对象无法被识别
typeof /abc/g === "object"; // 正则表达式无法被识别
typeof new Map() === "object"; // 集合类型无法被识别
typeof new Error() === "object"; // 错误实例无法被识别
2. NaN 的数字假象
在计算逻辑中,非法的数值计算(如 "abc" * 2 或 parseInt("xyz"))会产生 NaN(Not a Number)。然而:
console.log(typeof NaN); // "number"
如果仅通过 typeof x === 'number' 校验数值参数,NaN 就会顺利蒙混过关,导致下游数学计算彻底失效。
3. 构造函数创建的包装对象
如果使用 new 操作符调用基础类型的包装构造函数,创建出来的不再是原始值,而是一个包装对象:
const strPrimitive = "hello";
const strObject = new String("hello");
console.log(typeof strPrimitive); // "string"
console.log(typeof strObject); // "object" ⚠️ 容易引发预期之外的条件误判
四、精准判断 JavaScript 数据类型的工业级方案
由于 typeof 存在上述盲区,我们在编写通用工具库、表单校验或组件底层逻辑时,必须根据场景搭配更严谨的判断策略。
1. 针对 null 的唯一可靠判定:全等比较 (===)
判断一个值是否为 null,不需要任何复杂函数,直接使用全等运算符:
function isNull(val) {
return val === null;
}
2. 针对普通对象的安全检测模式
如果你只想确认某个值是“非空的有效对象”,必须显式排除 null:
function isPlainObject(val) {
return typeof val === "object" && val !== null && !Array.isArray(val);
}
3. 针对数组的标准检测:Array.isArray()
ES5 引入的 Array.isArray() 是判断数组的金科玉律。它甚至能够跨越多个 <iframe> 窗口边界(跨 realm),而传统的 instanceof Array 在多全局环境下会由于原型构造器不同而失效。
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
4. 终极万能方案:Object.prototype.toString.call()
当需要精准区分所有内置数据类型(包括原始类型与各类复杂对象)时,业界通用且最稳固的手段是借用 Object.prototype.toString 方法。
根据 ECMAScript 规范,任何值传入 toString 时,如果未被自身重写的方法拦截,都会返回形如 [object Tag] 的规范字符串:
const toTypeString = (val) => Object.prototype.toString.call(val);
console.log(toTypeString(null)); // "[object Null]" 🌟 完美识别 null!
console.log(toTypeString(undefined)); // "[object Undefined]"
console.log(toTypeString([])); // "[object Array]"
console.log(toTypeString(new Date())); // "[object Date]"
console.log(toTypeString(/test/)); // "[object RegExp]"
console.log(toTypeString(new Map())); // "[object Map]"
console.log(toTypeString(new Set())); // "[object Set]"
console.log(toTypeString(123n)); // "[object BigInt]"
console.log(toTypeString(Symbol())); // "[object Symbol]"
基于此原理,我们可以封装出一个在生产环境中开箱即用的类型提取通用函数:
/**
* 获取任意变量的准确类型字符串(小写)
* @param {*} value 任意待检测值
* @returns {string} 如 'null', 'array', 'date', 'regexp', 'number' 等
*/
function getType(value) {
// 兼容 null 与 undefined 的快速返回
if (value === null) return "null";
if (value === undefined) return "undefined";
// 利用 Object.prototype.toString 提取核心标签
const rawTag = Object.prototype.toString.call(value);
// 正则提取 "[object Tag]" 中的 Tag 并转小写
return rawTag.replace(/^\[object\s(.*)\]$/, "$1").toLowerCase();
}
// 验证测试结果
console.log(getType(null)); // "null"
console.log(getType([])); // "array"
console.log(getType(new Date())); // "date"
console.log(getType(42)); // "number"
console.log(getType(NaN)); // "number"(可通过 Number.isNaN 进一步辅助)
五、四大类型检测方案横向对比与总结
为了在实际开发中做到选型心中有数,我们将常用的四种判断方式(typeof、instanceof、constructor 与 Object.prototype.toString)进行全方位横向对比:
选型建议与速记口诀
- 查原始类型与函数:直接用
typeof,性能最高、语法最简,注意排查NaN(用Number.isNaN)。 - 查空值:使用全等
val === null或val === undefined。 - 查数组:唯一指定方法
Array.isArray(val)。 - 查自定义类实例:使用
instanceof MyClass,通过原型链追踪继承关系。 - 编写高鲁棒性基础库或未知类型识别:统一使用
Object.prototype.toString.call(val)。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18








