一个简单的问题,背后是 30 年的历史
写 JavaScript 的时候,估计你也见过这个神奇的现象:
javascript
typeof null // 'object'
这是什么鬼?null 不是"空"吗,怎么就成了对象?
刚学 JavaScript 那会儿,我还以为 null 真的是个特殊对象,后来才发现------这是个 bug ,而且是 30 年前就存在的 bug,直到今天都没修复。
更魔幻的是:这个 bug 永远不会被修复。
先抛几个问题,看看你是不是也有同样的困惑:
- 为什么
typeof null会返回'object'? - 这个 bug 是怎么产生的?
- 既然知道是 bug,为什么不修复?
- 怎么正确判断一个值是不是真的对象?(这个最实用)
JavaScript 的类型系统:一切从二进制说起
要理解这个 bug,得先看看 JavaScript 底层是怎么存储类型信息的。
现代引擎的实现:指针标记(Pointer Tagging)
现代浏览器(比如 Firefox)用一种叫 "指针标记"(Pointer Tagging) 的技术,在 64 位值里同时编码类型和数据:
┌─────────────────────────────────────────────────────────────┐
│ 高位(类型标签) │ 低位(值或地址) │
└─────────────────────────────────────────────────────────────┘
举个例子,布尔值在内存里长这样:
| 值 | 类型标签 | 数据 |
|---|---|---|
false |
0xFFFE... |
0x000000000000 |
true |
0xFFFE... |
0x000000000001 |
高位用来标记类型,低位存具体的值。这样设计的好处是:类型信息和数据可以紧凑地存在一起,不需要额外空间。
1995 年的 Netscape:32 位标记方案
但是!故事要回到 30 年前。
那时候 Brendan Eich(JavaScript 之父)在 Netscape 工作,被要求在 10 天内 设计出一门语言:
- 要简单(不需要编译器)
- 能操作 DOM
- 要能跟 Java 扯上关系(当时 Java 很火)
于是 JavaScript(最早叫 Mocha、LiveScript)就这么诞生了。
Netscape 的 JavaScript 引擎用的是 32 位标记方案,用低 3 位来标记类型:
c
#define JSVAL_OBJECT 0x0 // 000
#define JSVAL_INT 0x1 // 001
#define JSVAL_DOUBLE 0x2 // 010
#define JSVAL_STRING 0x4 // 100
#define JSVAL_BOOLEAN 0x6 // 110
内存布局大概是这样:
| 类型 | 类型标签 | 内存表示 | 示例 |
|---|---|---|---|
| Object | 000 |
[29位指针][000] |
0x12345000 |
| Integer | 001 |
[29位整数][001] |
0x00006401 (42) |
| String | 100 |
[29位指针][100] |
0x78901004 |
| Boolean | 110 |
[29位值][110] |
0x00000006 (true) |
为什么对象的标签是 000?
这不是随便选的,而是跟 内存对齐 有关。
在 32 位系统里:
- CPU 每次加载 4 字节(32 位)数据
- 对象分配在堆上,地址必须是 4 的倍数(内存对齐要求)
- 但实际上为了留出 3 位做标签,地址必须是 8 的倍数
这意味着所有对象的地址末尾必然是 000(二进制):
ini
8 = 0b00001000 (末尾 3 位是 000)
16 = 0b00010000 (末尾 4 位是 0000)
24 = 0b00011000 (末尾 3 位是 000)
所以 000 被自然地用来表示对象类型。
Bug 是怎么产生的?
null 的二进制表示
在 C 语言里(JavaScript 引擎是用 C 写的),null 通常定义为:
c
#define NULL ((void*)0)
就是一个指向地址 0 的空指针。在 JavaScript 里也是类似的:
c
#define JSVAL_NULL OBJECT_TO_JSVAL(0)
null 的二进制表示就是:
arduino
0x00000000 // 全是 0
末尾 3 位?当然也是 000。
typeof 的实现代码
Netscape 1.3 的 typeof 实现是这样的(简化版):
c
JS_TypeOfValue(JSContext *cx, jsval v) {
if (JSVAL_IS_VOID(v)) {
return JSTYPE_VOID; // undefined
} else if (JSVAL_IS_OBJECT(v)) { // 🔴 问题在这
// ...判断是不是函数...
return JSTYPE_OBJECT;
} else if (JSVAL_IS_NUMBER(v)) {
return JSTYPE_NUMBER;
} else if (JSVAL_IS_STRING(v)) {
return JSTYPE_STRING;
} else if (JSVAL_IS_BOOLEAN(v)) {
return JSTYPE_BOOLEAN;
}
}
关键在 JSVAL_IS_OBJECT 这个宏:
c
#define JSVAL_TAG(v) ((v) & 0x7) // 取低 3 位
#define JSVAL_IS_OBJECT(v) (JSVAL_TAG(v) == JSVAL_OBJECT)
它只检查低 3 位是不是 000。问题来了:
- 对象 :地址末尾是
000✅ - null :
0x00000000,末尾也是000✅
所以 null 被误判成对象了!
能修吗?当然能!
更魔幻的是,代码里其实有检查 null 的宏:
c
#define JSVAL_IS_NULL(v) ((v) == JSVAL_NULL)
只要在 typeof 里先判断一下就行:
c
JS_TypeOfValue(JSContext *cx, jsval v) {
if (JSVAL_IS_NULL(v)) { // 🟢 加这一行就行
return JSTYPE_NULL;
} else if (JSVAL_IS_VOID(v)) {
return JSTYPE_VOID;
} else if (JSVAL_IS_OBJECT(v)) {
// ...
}
// ...
}
这就是一个一行代码能修的 bug。
为什么 30 年都没修?
1. 历史包袱太重
到发现这个问题的时候,已经有 数百万个网站 用上了 JavaScript,而且很多代码已经 依赖这个 bug:
javascript
// 很多老代码这么写
if (typeof value === 'object') {
// 假设这里包括了 null 的情况
}
如果突然修复,这些代码都会挂掉。
2. 2013 年的修复提案被拒
ECMAScript 标准委员会在 2013 年收到过修复提案,但被拒了。
理由很简单:向后兼容性。
修复这个 bug 会破坏太多现有代码,得不偿失。
3. 有变通方案
反正有办法正确判断:
javascript
// 方案 1:额外检查 null
if (value !== null && typeof value === 'object') {
// 这才是真的对象
}
// 方案 2:用 Object.prototype.toString
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call({}) // '[object Object]'
既然能绕过去,就没必要冒险修复了。
实际使用建议
正确判断对象的方法
javascript
// ✅ 推荐:显式排除 null
function isObject(value) {
return value !== null && typeof value === 'object';
}
// ✅ 更严格:只判断纯对象(排除数组、Date 等)
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
// ❌ 错误:会把 null 当成对象
function isBadObject(value) {
return typeof value === 'object'; // null 会返回 true
}
其他 typeof 的坑
除了 null,typeof 还有其他问题:
javascript
// 数组和对象都是 'object'
typeof [] // 'object'
typeof {} // 'object'
// 函数是 'function'(这个倒是对的)
typeof function() {} // 'function'
// NaN 是 'number'(哲学问题)
typeof NaN // 'number'
所以判断类型的时候,别只靠 typeof:
javascript
// 判断数组
Array.isArray([]) // true
// 判断 null
value === null // 最直接
// 判断 undefined
value === undefined // 或 typeof value === 'undefined'
// 判断纯对象
Object.prototype.toString.call(value) === '[object Object]'
总结
研究完 typeof null === 'object' 这个 bug,我的理解是:
原理层面:
- JavaScript 用低位标记类型,对象的标记是
000 null在内存里是0x00000000,末尾也是000typeof只检查标记,所以把null误判成对象
历史层面:
- 1995 年 Netscape 时代的设计妥协
- 一行代码能修,但修了会破坏无数网站
- 2013 年修复提案被拒,理由是向后兼容
实用层面:
- ✅ 用
value !== null && typeof value === 'object'判断对象 - ✅ 用
Object.prototype.toString.call()更精确 - ❌ 别直接用
typeof value === 'object'
这个 bug 会一直存在下去,但只要知道它的来龙去脉,就能写出更可靠的代码。
毕竟,了解历史,才能不被历史绊倒。
相关文档: