typeof null === 'object':JavaScript 最古老的 bug 为何 30 年无法修复?

一个简单的问题,背后是 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
  • null0x00000000,末尾也是 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 的坑

除了 nulltypeof 还有其他问题:

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,末尾也是 000
  • typeof 只检查标记,所以把 null 误判成对象

历史层面

  • 1995 年 Netscape 时代的设计妥协
  • 一行代码能修,但修了会破坏无数网站
  • 2013 年修复提案被拒,理由是向后兼容

实用层面

  • ✅ 用 value !== null && typeof value === 'object' 判断对象
  • ✅ 用 Object.prototype.toString.call() 更精确
  • ❌ 别直接用 typeof value === 'object'

这个 bug 会一直存在下去,但只要知道它的来龙去脉,就能写出更可靠的代码。

毕竟,了解历史,才能不被历史绊倒


相关文档

相关推荐
可触的未来,发芽的智生4 小时前
触摸未来2025-10-25:蓝图绘制
javascript·python·神经网络·程序人生·自然语言处理
__WanG4 小时前
如何编写标准StatefulWidget页面
前端·flutter
非凡ghost4 小时前
By Click Downloader(下载各种在线视频) 多语便携版
前端·javascript·后端
非凡ghost4 小时前
VisualBoyAdvance-M(GBA模拟器) 中文绿色版
前端·javascript·后端
非凡ghost4 小时前
K-Lite Mega/FULL Codec Pack(视频解码器)
前端·javascript·后端
麦麦大数据4 小时前
F034 vue+neo4j 体育知识图谱系统|体育文献知识图谱vue+flask知识图谱管理+d3.js可视化
javascript·vue.js·知识图谱·neo4j·文献·体育·知识图谱管理
LinXunFeng4 小时前
Flutter 多仓库本地 Monorepo 方案与体验优化
前端·flutter·架构
非凡ghost4 小时前
ProcessKO(查杀隐藏危险进程)多语便携版
前端·javascript·后端
yinuo5 小时前
你的网页还不会"看人"?3分钟让它拥有会追踪的眼睛
前端