8种语言类型与内部表示
JavaScript 有一个鲜为人知的事实:typeof null === 'object' 这个 bug 在 1995 年就已存在,至今无法修复,因为修复它会破坏全世界数以亿计的网页。
🔹 Level 1 · 你需要知道的
8种语言类型速查
JavaScript 有且只有 8 种语言类型。这不是约定俗成,而是 ECMAScript 规范第 6 节的硬性规定。
| 类型 | typeof 返回值 | 字面量示例 | 可否为 null |
|---|---|---|---|
| Undefined | "undefined" |
undefined |
否 |
| Null | "object" ⚠️ |
null |
— |
| Boolean | "boolean" |
true, false |
否 |
| String | "string" |
"hello", '', `模板` |
否 |
| Number | "number" |
42, 3.14, NaN, Infinity |
否 |
| BigInt | "bigint" |
42n, 9007199254740993n |
否 |
| Symbol | "symbol" |
Symbol('desc') |
否 |
| Object | "object" |
{}, [], new Date() |
是 |
注意:函数是对象,但 typeof function(){} 返回 "function",这是规范的特殊豁免。
typeof 完整返回值表
typeof undefined // "undefined"
typeof null // "object" ← 历史 bug,无法修复
typeof true // "boolean"
typeof "hello" // "string"
typeof 42 // "number"
typeof 42n // "bigint"
typeof Symbol() // "symbol"
typeof {} // "object"
typeof [] // "object" ← 数组也是对象
typeof function(){} // "function" ← 规范特殊处理
typeof class Foo {} // "function" ← class 也是函数
typeof undeclaredVar // "undefined" ← 不报 ReferenceError!
隐式类型创建规则
以下操作会隐式创建特定类型的值:
// 创建 undefined
let x; // x 是 undefined
function f() {} // f() 返回 undefined
obj.nonexist // 访问不存在属性返回 undefined
void 0 // void 表达式总是返回 undefined
// 创建 null(只能显式)
let y = null;
// 创建 boolean
!!value // 双非运算
value ? a : b // 三元运算的条件求值
if (value) // if 语句的条件
arr.includes(x) // 返回 boolean
// 创建 number(隐式转换)
+"42" // 42(一元加号)
"6" * "7" // 42(乘法运算)
parseInt("42px") // 42(提取开头数字)
Number(true) // 1
Number(false) // 0
Number(null) // 0 ← 令人惊讶
Number(undefined) // NaN ← 令人惊讶
Number("") // 0 ← 令人惊讶
// 创建 string(隐式转换)
"" + 42 // "42"(加号有字符串优先)
`${someValue}` // 模板字符串调用 toString()
[1,2,3].join(",") // "1,2,3"
新手常犯的5个错误
错误1:用 == null 检查 undefined
// ✅ 正确:同时检查 null 和 undefined(唯一推荐用 == 的场景)
if (value == null) { ... } // value 为 null 或 undefined 时成立
// ❌ 错误:只检查 undefined,漏掉 null
if (value === undefined) { ... }
// ❌ 错误:typeof 检查太冗长
if (typeof value === 'undefined') { ... } // 只在 value 可能未声明时用
错误2:对 NaN 用 === 比较
// ❌ NaN 是唯一不等于自身的值
NaN === NaN // false!
// ✅ 使用 Number.isNaN(严格,不转换类型)
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false ← 不是数字但不是 NaN
// ⚠️ 全局 isNaN 有类型转换
isNaN("NaN") // true ← 先转 number 再判断,有误导性
isNaN("hello") // true ← "hello" 转 number 是 NaN
错误3:混淆 typeof 检查对象和 null
// ❌ 这段代码有 bug
function processObj(obj) {
if (typeof obj === 'object') {
obj.method(); // null 会导致 TypeError!
}
}
// ✅ 正确做法
function processObj(obj) {
if (obj !== null && typeof obj === 'object') {
obj.method();
}
}
错误4:用 typeof 检查数组
// ❌ typeof [] 是 "object",不能区分数组
typeof [] // "object"
// ✅ 用 Array.isArray
Array.isArray([]) // true
Array.isArray({}) // false
错误5:认为 undefined 可以被覆盖
// 在旧浏览器(ES5以前)undefined 可以被赋值
// undefined = 42; ← 这是旧代码的噩梦
// 现代 JS 中,undefined 是全局属性且不可写
// 但在局部作用域仍然可以遮蔽(不推荐)
function bad() {
let undefined = 42; // 局部遮蔽,但代码可读性极差
console.log(undefined); // 42
}
// ✅ 安全检查 undefined 的方式
typeof x === 'undefined' // 最安全,即使 x 未声明
x === void 0 // void 0 总是返回 undefined
🔸 Level 2 · 它是怎么运行的
V8 引擎的类型内部表示
V8 是 Chrome 和 Node.js 使用的 JavaScript 引擎。它使用了一套精妙的内存表示方案,在性能和灵活性之间取得平衡。
指针标记(Pointer Tagging)
V8 的所有值都通过一个机器字(64位系统上是64位)来表示。V8 使用指针的最低位来区分两类值:
V8 Tagged Value(64位):
最低位 = 0 → SMI(Small Integer,小整数)
[63位有符号整数][0]
高63位直接存储整数值,效率极高
最低位 = 1 → 堆指针(Heap Pointer)
[62位堆地址][01] 或其他标记
指向堆上分配的对象
Smi vs HeapNumber:数字的两种命运
小整数示例(42):
┌─────────────────────────────────────────────────────────┐
│ 64位 SMI 表示 42 │
│ [0000...0000 0101 0100 0] [最低位=0 表示SMI] │
│ 高63位 = 42 的二进制 标记位 │
└─────────────────────────────────────────────────────────┘
不占用堆内存,直接嵌入指针值,零额外分配开销
大整数或浮点数(3.14 或 2^31 以上):
┌─────────────────────────────────────────────────────────┐
│ HeapNumber 对象(堆上分配) │
│ ┌──────────┬────────────────────────────────────┐ │
│ │ Map指针 │ 64位 IEEE 754 double 值 │ │
│ │ (8字节) │ (8字节,存储 3.14 的浮点表示) │ │
│ └──────────┴────────────────────────────────────┘ │
│ 总共约 24 字节(包含对象头开销) │
└─────────────────────────────────────────────────────────┘
Smi 的范围:在64位 V8 中,Smi 使用63位有符号整数,范围是 -2^62 到 2^62-1(约 ±4.6 × 10^18)。超出此范围的整数会变成 HeapNumber。
性能含义:整数循环中,如果计数器保持在 Smi 范围内,性能显著优于使用浮点数。
字符串的两种内部表示
V8 根据字符串的创建方式选择不同的内部表示:
SeqString(连续内存字符串):
┌──────────────────────────────────────────────────────────┐
│ 适用于:字面量字符串、短字符串拼接结果 │
│ │
│ Map指针 │ Hash │ Length │ C h a r a c t e r s ... │
│ (8字节) │(4字节)│(4字节) │ 连续的字符数据 │
│ │
│ 优点:随机访问 O(1),内存连续,CPU 缓存友好 │
│ 缺点:修改需要重新分配 │
└──────────────────────────────────────────────────────────┘
ConsString(链式拼接字符串):
┌──────────────────────────────────────────────────────────┐
│ 适用于:"a" + "b" + "c" 这样的多次拼接 │
│ │
│ Map指针 │ Length │ 左指针 → "hello" │
│ │ │ 右指针 → " world" │
│ │
│ 结构类似二叉树,只有在访问单个字符时才"展平" │
│ │
│ ConsString("hello world") │
│ / \ │
│ "hello" " world" │
│ │
│ 优点:拼接 O(1),不立即分配新内存 │
│ 缺点:随机访问需要遍历树,最坏 O(n) │
└──────────────────────────────────────────────────────────┘
实践影响:在循环中用 += 拼接大量字符串时,V8 会先创建 ConsString 树,最终"扁平化"时才分配连续内存。这就是为什么大量拼接推荐用 Array.join('')——尽管现代 V8 已大幅优化了字符串拼接的性能。
undefined 和 null 的特殊处理
undefined 和 null 在 V8 中的表示:
┌──────────────────────────────────────────────────────┐
│ undefined:全局单例对象(Oddball 类型) │
│ 存储在 V8 根列表(Root List)中 │
│ 不在普通堆上分配,每次访问返回同一个指针 │
│ 内存地址固定,比较极快 │
│ │
│ null:另一个全局 Oddball 单例 │
│ 同样在根列表中 │
│ typeof null 返回 "object" 是因为历史上的类型标签 │
└──────────────────────────────────────────────────────┘
对象的 Map(Hidden Class)
每个 JS 对象都有一个隐藏的 Map 指针(在 V8 中叫做 Hidden Class,不是 JavaScript 的 Map 集合):
JS 对象的内存布局:
const obj = { x: 1, y: 2 };
┌─────────────────────────────────────────────────────┐
│ JS Object │
│ ┌────────────┬──────────────────────────────────┐ │
│ │ Map 指针 │ → Hidden Class 描述对象结构 │ │
│ │ (8字节) │ │ │
│ ├────────────┼──────────────────────────────────┤ │
│ │ Properties │ → 属性存储(可能内联或外部) │ │
│ │ 指针(8字节)│ │ │
│ ├────────────┼──────────────────────────────────┤ │
│ │ Elements │ → 数组元素存储 │ │
│ │ 指针(8字节)│ │ │
│ ├────────────┼──────────────────────────────────┤ │
│ │ x: 1 (SMI)│ 内联属性存储(最多4个) │ │
│ │ y: 2 (SMI)│ │ │
│ └────────────┴──────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Hidden Class 决定了属性的偏移量,使属性访问可以编译为
直接内存偏移,而不是哈希表查找,性能提升约 10 倍。
🔺 Level 3 · 规范怎么定义的
ECMAScript 规范第6节:数据类型与值
规范将类型分为两大类:
语言类型(Language Types):程序员可以操作的值的类型:
- Undefined、Null、Boolean、String、Symbol、BigInt、Number、Object
规范类型(Specification Types):规范内部算法使用的类型,不对应实际的 JS 值:
| 规范类型 | 用途 |
|---|---|
| Completion Record | 描述语句执行的结果(正常完成、break、continue、return、throw) |
| Reference Record | 描述属性引用(谁是 base,什么是 name,是否 strict) |
| Property Descriptor | 描述对象属性的特性(value、writable、enumerable、configurable) |
| Environment Record | 描述词法作用域绑定 |
| Abstract Closure | 规范内的函数抽象 |
| Data Block | 原始字节序列(用于 ArrayBuffer) |
Completion Record 详解
每个语句的执行都返回一个 Completion Record:
Completion Record 结构:
{
[[Type]]: normal | break | continue | return | throw
[[Value]]: ECMAScript 值 或 empty
[[Target]]: 字符串标签(用于带标签的 break/continue)或 empty
}
规范原文(ECMA-262 第 6.2.4 节):
The Completion Record Specification Type
The Completion Record specification type is used to explain the runtime propagation of values and control flow such as the behaviour of statements (break, continue, return and throw) that perform nonlocal transfers of control.
这意味着当你写:
function foo() {
return 42;
}
规范层面,return 42 语句产生 Completion Record { [[Type]]: return, [[Value]]: 42, [[Target]]: empty },调用方通过检查 [[Type]] 来决定下一步行为。
Reference Record 详解
属性访问和变量访问在规范中都表示为 Reference Record:
Reference Record 结构:
{
[[Base]]: ECMAScript值 | 环境记录 | unresolvable
[[ReferencedName]]: 字符串 | Symbol
[[Strict]]: 布尔值
[[ThisValue]]: ECMAScript值 | empty
}
例如 obj.foo 产生:
{
[[Base]]: obj,
[[ReferencedName]]: "foo",
[[Strict]]: false,
[[ThisValue]]: empty
}
这就是为什么 typeof undeclaredVar 不报错:规范在 typeof 运算符的处理中特别检查了 Reference Record 的 [[Base]] 是否为 unresolvable,如果是,直接返回 "undefined" 而不是抛出 ReferenceError。
语言类型的规范定义
规范对 8 种语言类型的核心定义(第 6.1 节):
6.1.1 The Undefined Type:
The Undefined type has exactly one value, called undefined. Any variable that has not been assigned a value has the value undefined.
6.1.2 The Null Type:
The Null type has exactly one value, called null.
6.1.3 The Boolean Type:
The Boolean type represents a logical entity having two values, called true and false.
6.1.4 The String Type(重要部分):
The String type is the set of all ordered sequences of zero or more 16-bit unsigned integer values ("elements")... Each element is considered to be a UTF-16 code unit value.
这明确说明 JS 字符串是 UTF-16 码元(Code Unit)序列,不是 Unicode 码点序列。这是 '😀'.length === 2 的根本原因。
6.1.6.1 The Number Type:
The Number type has exactly 18437736874454810627 (that is, 2^64 − 2^53 + 3) values...
这 18 千万亿个值包括:
- 2^53 × 2^971 个有限非零值(正负各一半)
- 正零(+0)和负零(-0)
- 正无穷大(+∞)和负无穷大(-∞)
- NaN(实际上有 2^53 - 2 个 NaN 位模式,但规范视为一个值)
💎 Level 4 · 边界与陷阱
陷阱1:typeof null === 'object' 的32位真相
这是 JavaScript 最著名的 bug,来自1995年 Netscape 的第一版 JavaScript 实现。
32位时代的类型标签系统:
早期 JS 引擎(32位)使用指针的最低3位作为类型标签:
000 = 对象(Object)
001 = 整数(Integer)
010 = 浮点数(Double)
100 = 字符串(String)
110 = 布尔值(Boolean)
特殊值:
null → 使用了 C 语言的 NULL 指针(0x00000000)
最低3位 = 000 → 被误判为对象类型!
Brendan Eich 的原话(2012年):
typeof null returning "object" is a consequence of Null values, from JavaScript's inception, being represented as a machine null pointer. In C, sizeof(NULL) is sizeof(void *) == sizeof(integer on most CPUs). The tag bits used for distinguishing types showed null pointers as objects.
为什么不修复:ECMAScript 委员会(TC39)在 ES6 时曾提案修复此 bug(typeof null === 'null'),但被拒绝,原因是:
- 全球数亿个网页依赖
typeof null === 'object' - 破坏性变更无法向后兼容
正确检测 null 的方式:
// ❌ 不可靠
typeof value === 'object' // null 也满足
// ✅ 明确检查 null
value === null
// ✅ 检查是否为非空对象
value !== null && typeof value === 'object'
陷阱2:typeof function(){} 返回 'function' 而非 'object'
函数在 ECMAScript 规范中是对象(Object 类型的子类型),但 typeof 特殊处理了可调用对象。
规范 13.5.3 节(typeof UnaryExpression)的判断逻辑:
typeof 算法(简化版):
1. 如果 val 是 Undefined → return "undefined"
2. 如果 val 是 Null → return "object"
3. 如果 val 是 Boolean → return "boolean"
4. 如果 val 是 Number → return "number"
5. 如果 val 是 String → return "string"
6. 如果 val 是 Symbol → return "symbol"
7. 如果 val 是 BigInt → return "bigint"
8. 如果 val 是 Object:
a. 如果 val 有 [[Call]] 内部方法 → return "function" ← 关键!
b. 否则 → return "object"
[[Call]] 内部方法是函数对象的标志。普通对象没有 [[Call]],函数对象有。所有通过 function、=> 或 class 创建的函数都有 [[Call]]:
typeof function(){} // "function" - 普通函数,有 [[Call]]
typeof (() => {}) // "function" - 箭头函数,有 [[Call]]
typeof class Foo {} // "function" - class 语法糖,有 [[Call]] 和 [[Construct]]
typeof {} // "object" - 普通对象,无 [[Call]]
typeof [] // "object" - 数组,无 [[Call]]
陷阱3:undefined == null 为 true 的规范硬编码
这不是任何类型转换算法的结果,而是规范在抽象相等比较(Abstract Equality Comparison)中的硬编码特例:
规范 7.2.13 节(IsLooselyEqual)第3、4条:
规范原文:
3. If x is null and y is undefined, return true.
4. If x is undefined and y is null, return true.
这意味着 null == undefined 的判断完全绕过了 ToPrimitive、ToNumber 等任何转换算法,直接返回 true。
null == undefined // true(规范第3条)
undefined == null // true(规范第4条)
null === undefined // false(严格相等,类型不同)
// 利用这个特性做安全的 null/undefined 检查
function isNullish(val) {
return val == null; // 同时捕获 null 和 undefined
}
isNullish(null) // true
isNullish(undefined) // true
isNullish(0) // false
isNullish("") // false
isNullish(false) // false
// 但注意 null 不等于其他任何假值
null == 0 // false
null == "" // false
null == false // false
陷阱4:Object(null) 和 Object(undefined) 返回空对象
当 Object() 构造函数接收 null 或 undefined 时,行为与接收其他值不同:
Object(null) // {} ← 返回空对象!不是包装 null
Object(undefined) // {} ← 返回空对象!不是包装 undefined
Object(42) // Number {42} ← 返回 Number 包装对象
Object("hello") // String {"hello"} ← 返回 String 包装对象
Object(true) // Boolean {true} ← 返回 Boolean 包装对象
规范 20.1.1.1(Object(value))的算法:
1. 如果 NewTarget 不是 undefined,返回 OrdinaryCreateFromConstructor(NewTarget, ...)
2. 如果 value 是 undefined 或 null:
return OrdinaryObjectCreate(%Object.prototype%) ← 返回普通空对象
3. return ToObject(value) ← 其他值包装
实际应用场景:
// 安全地将任意值转为对象(即使是 null/undefined)
const safeObj = Object(potentiallyNull); // 不会抛错
// 但这通常不是好实践,更好的方式是:
const safeObj = potentiallyNull ?? {};
// Object() 的真正用途:检查某值是否为对象类型
function isObject(val) {
return val === Object(val); // 原始类型 Object(val) !== val
}
isObject({}) // true
isObject([]) // true
isObject(null) // false!Object(null) 是新对象,不等于 null
isObject(undefined) // false
isObject(42) // false(Number包装对象 !== 42)
本章小结
-
8种语言类型是规范的核心:Undefined、Null、Boolean、String、Number、BigInt、Symbol、Object。
typeof的返回值并不完全对应这8种(null返回"object",函数返回"function")。 -
V8 用 Smi 优化小整数性能:范围内的整数直接嵌入指针(无堆分配),超出范围的数字变成 HeapNumber(堆对象,约24字节)。在性能关键路径上保持整数在 Smi 范围内可以避免不必要的堆分配。
-
字符串有两种内部表示:SeqString 内存连续、随机访问 O(1);ConsString 是链式拼接树、拼接 O(1) 但随机访问 O(n),V8 会在适当时机扁平化。
-
typeof null === 'object'是30年前的 bug:源于32位机器上 null 指针的类型标签被误判为对象,TC39 决定永远不修复它,因为修复会破坏数亿网页。 -
规范类型(Completion Record、Reference Record 等)是引擎内部概念:它们解释了
return/break/throw的传播机制、属性访问的解析方式,以及typeof undeclaredVar不报错的原因。理解规范类型是深入理解 JS 引擎行为的关键。