编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

    • 综合案例

    • 专栏博客

      • README
      • 引擎解析编译执行
      • 隐藏类与回收机制
      • 类型隐式转换精算
        • 1. 案例与疑问引入
          • 1.1 [] == ![
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构全景概览
          • 2.1 8 种类型
          • 2.2 类型系统的三条转
        • 3. typeof标签位
          • 3.1 V8 内部类型标
          • 3.2 为什么 null
          • 3.3 为什么 func
        • 4. ToPrimit
          • 4.1 hint 的三种值
          • 4.2 valueOf
          • 4.3 Symbol.t
        • 5. 类型转换算法
          • 5.1 ToNumber
          • 5.2 ToString
          • 5.3 ToBoolean
        • 6. 等号全推导
          • 6.1 ECMA-262
          • 6.2 名人堂级反直觉案
          • 6.3 === 为什么比
        • 7. 浮点精度详解
          • 7.1 双精度 64 位
          • 7.2 0.1+0.2
          • 7.3 四种解决方案与各
        • 8. BigInt深度
          • 8.1 Smi vs H
          • 8.2 BigInt
          • 8.3 数值操作的安全边
        • 9. Intl 国际化
          • 9.1 DateTime
          • 9.2 ICU 库依赖与
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一个能处理 Na
          • 10.3 设计哲学回扣
          • 10.4 速查表:==
      • 作用域链闭包原理
      • 函数绑定规则组合
      • 原型链语法糖本质
      • 代理与元编程协议
      • 事件循环承诺机制
      • 工作线程并发调度
      • 页面渲染像素原理
      • 网络接口存储架构
      • 服务端运行时编程
      • 模块系统双轨操作
      • 现代工程链三件套
      • 设计模式函数哲学
      • 跨端架构终局总结
  • CodeX
  • JavaScript入门
  • 专栏博客
杨充
2026-06-11
目录

类型隐式转换精算

# 03.类型隐式转换精算

📍 上接第 02 篇《隐藏类与分代垃圾回收》。对象的内存模型已了然。本文攻破 JS 的「类型黑洞」——typeof null === 'object' 根源在哪?0.1+0.2 为什么不等于 0.3?[] == ![] 为什么是 true?

# 目录介绍

  • 1. 案例与疑问引入
    • [1.1 [] ==
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构全景概览
    • 2.1 8 种类型
    • 2.2 类型系统的三条转
  • 3. typeof标签位
    • 3.1 V8 内部类型标
    • 3.2 为什么 null
    • 3.3 为什么 func
  • 4. ToPrimit
    • 4.1 hint 的三种值
    • 4.2 valueOf
    • 4.3 Symbol.t
  • 5. 类型转换算法
    • 5.1 ToNumber
    • 5.2 ToString
    • 5.3 ToBoolean
  • 6. 等号全推导
    • 6.1 ECMA-262
    • 6.2 名人堂级反直觉案
    • 6.3 === 为什么比
  • 7. 浮点精度详解
    • 7.1 双精度 64 位
    • 7.2 0.1+0.2
    • 7.3 四种解决方案与各
  • 8. BigInt深度
    • 8.1 Smi vs H
    • 8.2 BigInt
    • 8.3 数值操作的安全边
  • 9. Intl 国际化
    • 9.1 DateTime
    • 9.2 ICU 库依赖与
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一个能处理 Na
    • 10.3 设计哲学回扣
    • 10.4 速查表:==

# 1. 案例与疑问引入

# 1.1 [] == ![

在 StackOverflow 上有一个被浏览超过 300 万次的问题——一句话就能让初学 JS 的人怀疑人生:

console.log([] == ![]);  // → true
1

第一次看到这一行的人反应通常是:"等等,一个空数组等于它自己的取反?这怎么可能?"

但这在 JS 里不仅是可能的,而且是严格遵循规范算法推导出来的必然结果。更令人吃惊的是,这个等式背后调用了 ECMA-262 规范中的 四条抽象操作——每一条都是 JS 类型系统的核心构件。

# 1.2 顺藤摸到根因

一步步推导 [] == ![] 的完整执行过程(每条推导都标注对应规范操作):

[] == ![]

步骤 1:先算右侧的 ![] —— 逻辑非运算符
  ![] → 根据 ToBoolean 规则
       → [] 是 truthy(因为不是 falsy 八值之一)
       → !truthy → false
  现在等式变成:[] == false

步骤 2:根据 == 规则(ECMA-262 §7.2.15 规则 7):
  "如果 Type(y) 是 Boolean  →  y = ToNumber(y)"
  → [] == ToNumber(false)
  → [] == 0

步骤 3:根据 == 规则 9:
  "如果 Type(x) 是 Object 且 Type(y) 是 Number/String/Symbol"
  "→ x = ToPrimitive(x)"
  → ToPrimitive([]) == 0

步骤 4:ToPrimitive([])
  → hint = "default"(因为 == 运算符对对象用 default hint)
  → 调用 [].valueOf() → 返回 [] 自身,不是基本类型
  → 再调用 [].toString()
  → [].toString() → ""
  现在等式变成:"" == 0

步骤 5:根据 == 规则 5:
  "如果 Type(x) 是 String 且 Type(y) 是 Number"
  "→ x = ToNumber(x)"
  → ToNumber("") == 0
  → 0 == 0

步骤 6:同类型→严格比较 → true ✓
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

五步转换把一个看似荒谬的等式论证为完全符合规范的结果。 这个推导过程包含了 JS 类型系统的四块核心积木——ToBoolean、ToNumber、ToPrimitive 和 == 抽象相等算法——本文会把每一块都拆开看。

# 1.3 我们要回答什么

本文围绕 JS 类型系统的"隐式转换黑洞",追问六个问题:

  1. ① typeof 遗产:typeof null === 'object'——这个二十多年的 bug 为什么修不了?
  2. ② ToPrimitive:为什么 1 + {} 和 {} + 1 结果完全不同?一个对象怎么被"剥皮"成基本类型?
  3. ③ ToNumber/ToString/ToBoolean:为什么 Number(" ") 返回 0 而不是 NaN?falsy 到底有几个?
  4. ④ == 全算法:规范里那 15 条抽象相等规则的完整推导——每条对应一个反直觉案例。
  5. ⑤ 浮点精度:0.1 + 0.2 !== 0.3——从 IEEE 754 的 64 位位布局一路讲到二进制尾数截断。
  6. ⑥ BigInt + 数值极限:为什么 9999999999999999 变成了 10000000000000000?BigInt 内部和 Smi/HeapNumber 有什么区别?

最后,用一个能处理 NaN / -0 / 正则 / 循环引用 / BigInt 的 deepEqual 实现串起文中所有类型判断细节。


# 2. 架构全景概览

# 2.1 8 种类型

ECMA-262 定义了 7 种基本类型(Primitive) + 1 种对象类型(Object):

┌─────────────────────────────────────────────────────┐
│              JS 类型系统(ECMA-262 定义)               │
├─────────────────────────────────────────────────────┤
│                                                     │
│  基本类型(Primitive)——存在栈上,不可变                 │
│  ┌─────────┬──────────┬───────────────────────────┐ │
│  │ Number  │ typeof→"number"│ 64位IEEE754,含NaN/±Inf││
│  │ BigInt  │ typeof→"bigint"│ 任意精度整数            │ │
│  │ String  │ typeof→"string"│ UTF-16 编码,不可变      │ │
│  │ Boolean │ typeof→"boolean"│ true / false          │ │
│  │ Undefined│typeof→"undefined"│ 变量未赋值时的默认值  │ │
│  │ Null    │ typeof→"object"│ ← 著名的 bug          │ │
│  │ Symbol  │ typeof→"symbol"│ 唯一标识符              │ │
│  └─────────┴──────────┴───────────────────────────┘ │
│                                                     │
│  对象类型(Object)——存在堆上,可变                       │
│  ┌─────────┬──────────┬───────────────────────────┐ │
│  │ Object  │ typeof→"object"或"function"│ 一切非基本│ │
│  │         │          │ 类型都是对象              │ │
│  │ ┌───────────┐     │  Array/RegExp/Date/Map/  │ │
│  │ │ 函数是特殊的 │     │  Set/WeakMap/Promise...  │ │
│  │ │ 可调用对象   │     │  typeof fn → "function" │ │
│  │ └───────────┘     │                           │ │
│  └─────────┴──────────┴───────────────────────────┘ │
│                                                     │
└─────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 2.2 类型系统的三条转

JS 的隐式类型转换不是"随便转"——它有三条固定的抽象操作通道:

┌─────────────────────────────────────────────────┐
│          三条抽象转换通道(规范内部操作)            │
├─────────────────────────────────────────────────┤
│                                                 │
│  ToPrimitive(input, hint)                       │
│    → 任何值 → 剥皮成基本类型                      │
│    → 用于:==, +, 模板字面量, 属性键                │
│    → 这是"万物归基本类型"的唯一入口                 │
│                                                 │
│  ToNumber(argument)                             │
│    → 基本类型 → 数字                             │
│    → 用于:算术运算(-*/%), ==, 一元 +, Number()  │
│                                                 │
│  ToString(argument)                             │
│    → 基本类型 → 字符串                           │
│    → 用于:+, 模板字面量, String(), 属性键         │
│                                                 │
│  ToBoolean(argument)                            │
│    → 任何值 → 布尔                              │
│    → 用于:if/while/! / &&/|| / 三元运算符       │
│    → 最简单——只有 8 个值是 falsy                 │
│                                                 │
└─────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这三个操作不是独立存在的——ToPrimitive 是入口,拆出基本类型后,ToNumber/ToString 接手完成最终的转换。本文的 §4、§5、§6 就是沿着这条链路——从"怎么剥"(ToPrimitive)到"怎么转"(ToNumber/ToString/ToBoolean)到"== 怎么用"(抽象相等算法)——一层层解构。


# 3. typeof标签位

# 3.1 V8 内部类型标

疑惑:V8 在底层怎么区分"这个值是什么类型"?不可能每次都做字符串比较。

论证:V8(以及所有主流 JS 引擎)使用**指针标签位(Pointer Tagging)**技术——在 64 位架构上,内存地址的最低 2~3 位总是 0(因为内存对齐到 4 或 8 字节),引擎利用这几位嵌入类型标签:

V8 64 位架构的表示策略(简化):
┌─────────────────────────────────────────┐
│  Smi(小整数):                           │
│  ┌────────────────────────────────────┐  │
│  │      31 位整数值 (补码)     │  │0│  │  ← 最低位 = 0
│  └────────────────────────────────────┘  │
│  例:值 1  → 0x00000002 (= 1<<1 + 0)      │
│      值 -1 → 0xFFFFFFFE                  │
│                                          │
│  HeapObject(堆对象指针):                   │
│  ┌────────────────────────────────────┐  │
│  │      堆地址 (62 bits)         │  │1│  │  ← 最低位 = 1
│  └────────────────────────────────────┘  │
│  例:obj → 0x1234567890AB1               │
│      最低位 = 1 表示这是"堆对象指针"           │
│        → V8 读出地址时右移一位获得真实指针        │
│                                          │
│  特殊值:                                  │
│  undefined → 0x000000000000000A (特殊常量)  │
│  null      → 0x0000000000000006 (特殊常量)  │
│  true      → 0x000000000000000E           │
│  false     → 0x0000000000000002           │
└─────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

关键:typeof 的实现不是一个查表函数——它直接读变量的标签位:

// 伪代码(V8 简化版)
function typeof_internal(value) {
  if (isSmi(value)) return 'number';
  if (isHeapObject(value)) {
    if (value == NullValue) return 'object';   // ← bug 在这里!
    if (isFunction(value)) return 'function';
    // ... 其他堆对象
    return 'object';
  }
  if (value == UndefinedValue) return 'undefined';
  if (value == TrueValue || value == FalseValue) return 'boolean';
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.2 为什么 null

疑惑:typeof null === 'object' 这个著名的 bug 二十多年了——为什么没人修?

论证:

回到 1995 年,JavaScript 在 Netscape 中用 10 天写出。在当时的实现中,值的类型标签存在 32 位值的低 1~3 位:

1995 年 JS 引擎的类型标签(32 位):
┌──────────────────────────────────────┐
│  类型标签(低 3 位)  │  对应的值           │
├──────────────────────────────────────┤
│  000                 │  对象 (Object)      │
│  001                 │  整数 (Int)         │
│  010                 │  浮点 (Double)      │
│  100                 │  字符串 (String)    │
│  110                 │  布尔 (Boolean)     │
│  000                 │  ...                │
│                      │                     │
│  null 被表示为: 0x00000000 (全零)           │
│  最低 3 位 → 000 → typeof 判定为 Object!   │
└──────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

null 在 32 位体系下被表示为零地址(全 0 的机器字)——这恰好和 Object 类型标签(000)重合。typeof 的实现用位掩码判断类型,结果 null 被误判为 Object。

为什么修不了?

// 假设 V8 有一天修复了这个 bug——typeof null → 'null'
// 所有依赖旧行为的代码全部崩溃:

// 老代码遍地都是:
if (typeof val === 'object' && val !== null) { ... }
// 一旦 typeof null 变成 'null',上面这种判断不会崩溃——
// 但 typeof null 从 'object' 变成 'null' 会导致:
// 1) 所有用 if (typeof x === 'object') 来检查"是否可以安全调用 x 的方法"的地方
//    需要重新考虑 null 是否也通过了检查
// 2) 反向后兼容——已经有无数 npm 包、CSS-in-JS 库、路由库做过了 null 特殊处理
//
// TC39 在 2015 年严肃讨论过"加 typeof null === 'null'"
// 结论:web 上已经挂了太多依赖 typeof null === 'object' 的代码
//      修复 bug 本身会引入更多的 bug → 不值得
1
2
3
4
5
6
7
8
9
10
11
12
13
14

结论:这不仅是"历史 bug",而是 "bug 已成为事实标准" 的经典案例。typeof null 永远不会被修复——不是因为开发者不在乎,而是因为修复本身的破坏性大于 bug 的困扰程度。

# 3.3 为什么 func

如果函数是 Object 的一个子类型,为什么 typeof 要单独给它返回 "function"?

答案是——不是因为规范规定的类型多出来一种,而是历史实用主义。Brendan Eich 在 1995 年实现 JS 时,为函数单独做了标签(110 或类似值),让 typeof fn 返回 "function"——因为"判断一个值是否是函数"是 JS 里最高频的操作之一。如果 typeof 统一返回 "object",每个调用前都要额外判断 key in obj ? fn() : obj[key] 这种模式会变得非常丑陋。

规范的态度:ECMA-262 规定 JS 有 7 种 + 1 种 = 8 种类型。"function" 不是一种独立的语言类型——它是 typeof 对"可调用对象"的实用主义特判。


# 4. ToPrimit

# 4.1 hint 的三种值

疑惑:1 + {} 和 {} + 1 为什么结果完全不同?

console.log(1 + {});    // → "1[object Object]"
console.log({} + 1);    // → "[object Object]1" …等等,也可能是 NaN 或 1?
1
2

答案在 ToPrimitive 的 hint(暗示) 参数中:

ToPrimitive(input, hint) 的 hint 有三种值:

1) hint = "string"
   触发场景:模板字面量 `${obj}`、String(obj)、obj[key](属性键)
   目的:把对象转为字符串
   → obj.toString() 先调用,再 valueOf()
   
2) hint = "number"
   触发场景:算术运算(除了 +)、Number(obj)、比较运算(< >)
   目的:把对象转为数字
   → obj.valueOf() 先调用,再 toString()
   
3) hint = "default"
   触发场景:+ 运算符、== 运算符(与基本类型比较时)
   目的:不确定期望什么类型——"让对象自己决定"
   → 行为与 hint = "number" 相同(先 valueOf 后 toString)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

为什么 {} 和 + 在不同的位置结果不同?

1 + {}  
// → 1 + ToPrimitive({}, "default")  // hint = "default"
// → 1 + "[object Object]"           // {}.valueOf()→{}(非基本类型)
//                                    // {}.toString()→"[object Object]"
// → "1[object Object]"              // + 运算符有一方是字符串 → 字符串拼接

{} + 1
// ← 这取决于上下文!{}
// 如果 {} 被解析为代码块(statement position)→ {} 是空代码块
//   → +1 → 1
// 如果 {} 被解析为表达式(expression position,如 eval 中)→ {} 是空对象
//   → "[object Object]1"
// Chrome DevTools 中直接输入:{} 被当作代码块 → 1
// 加上括号:( {} ) + 1 → "[object Object]1"  ← 括号强制表达式上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.2 valueOf

每一种内置对象都有自己的 ToPrimitive 实现:

对象类型 valueOf() 返回 toString() 返回 hint="number" 首先调 hint="string" 首先调
Object 自身(非基本类型)→ 继续调 toString "[object Object]" valueOf → toString toString → valueOf
Array 自身 join(",") —— [1,2].toString() = "1,2" valueOf → toString toString → valueOf
Date 时间戳(毫秒数) 完整日期字符串 toString → valueOf(Date 特例!) toString → valueOf
Number 对象 包装的数字值 数字的字符串形式 valueOf 就返回了基本类型 toString→valueOf
String 对象 包装的字符串值 包装的字符串 valueOf 就返回了基本类型 toString→valueOf

Date 的特例:

// Date 的 ToPrimitive 中,hint="default" 时先调 toString
// 这是因为在绝大多数场景中,Date 期望被转为字符串
const now = new Date();
console.log(now + 1);
// → "Fri Jun 12 2026 ..." + "1" = 字符串拼接
// 而不是:now.valueOf() + 1 → 时间戳 + 1 → 一个巨大的数字
// Date 是唯一 hint="default" 时先调 toString 的内置对象
1
2
3
4
5
6
7

# 4.3 Symbol.t

ES6 引入的 Symbol.toPrimitive 完全绕过 valueOf/toString 的调用顺序——最高优先级:

const obj = {
  valueOf() { return 100; },
  toString() { return 'hello'; },
  [Symbol.toPrimitive](hint) {
    console.log('hint:', hint);
    if (hint === 'number') return 42;
    if (hint === 'string') return 'world';
    return 'default_value';
  }
};

console.log(+obj);             // hint: number   → 42
console.log(`${obj}`);         // hint: string   → "world"
console.log(obj + '');         // hint: default  → "default_value"
// valueOf 和 toString 被完全忽略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

结论:ToPrimitive 的调用链是:

对象 → 有 Symbol.toPrimitive ?
         ├─ 是 → 调用它,传入 hint → 返回(必须是基本类型,否则抛 TypeError)
         └─ 否 → 根据 hint 决定先调 valueOf 还是 toString
                 → 如果第一次调用返回基本类型 → 直接返回
                 → 如果返回非基本类型 → 调另一个方法
                 → 如果都没返回基本类型 → TypeError
1
2
3
4
5
6

# 5. 类型转换算法

# 5.1 ToNumber

疑惑:Number(" ") 为什么返回 0 而不是 NaN?

论证:ToNumber 的规则远比"看起来"复杂。ECMA-262 规定的完整映射:

输入值 ToNumber 结果 为什么
undefined NaN 规范直接规定
null 0 规范直接规定(+null → 0)
true / false 1 / 0 布尔→数字的数学直觉
""(空字符串) 0 白空格全转为 0
" "(纯空格) 0 先 TrimString,变成 "",再转 0
"123" 123 数字字符串→对应数字
"123abc" NaN 包含非数字字符
"0x10" 16 十六进制识别
"Infinity" Infinity 特殊字符串→无穷大
Symbol() TypeError Symbol 不能隐式转数字
{} 或 [] NaN 或 0 先 ToPrimitive,再 ToNumber
[1] 1 [1].toString() = "1" → 1
[1,2] NaN [1,2].toString() = "1,2" → NaN

关键在于 StringToNumber 内部会先 Trim(去除首尾空格)再解析——这是 Number(" ") 返回 0 的原因:

Number("   ")
  → Trim 去掉前后空格 → ""
  → 空字符串 → 0
1
2
3

所有 ToNumber 特殊值速查:

// 这些会变成 0
+''           // → 0
+null         // → 0
+false        // → 0
+[]           // → 0([].toString() = "" → 0)

// 这些会变成 NaN
+undefined    // → NaN
+{}           // → NaN(({}).toString() = "[object Object]" → NaN)
+'hello'      // → NaN
+Symbol()     // → TypeError
1
2
3
4
5
6
7
8
9
10
11

# 5.2 ToString

ToString 是所有转换中最直观的一个——大多数情况就是"加引号":

输入 ToString 结果 注意
undefined "undefined" 直接映射
null "null" 直接映射
true / false "true" / "false" 直接映射
0 "0" 包括 -0 → "0"
NaN "NaN"
Infinity "Infinity"
1e21 "1e+21" 大数字用科学计数法
Symbol() TypeError 不能隐式转字符串(可 Symbol().toString())
{} "[object Object]" 通过 ToPrimitive
[1,2,3] "1,2,3" Array.prototype.toString 调用 join
function(){} "function(){}" 函数源码(截断)

# 5.3 ToBoolean

ToBoolean 是所有转换中最简单的——只有 8 个值是 falsy,其余所有值都是 truthy:

Falsy 八值:
┌──────────────┬──────────────────────┐
│ false        │ 布尔假值               │
│ 0, -0        │ 数字零、负零            │
│ 0n           │ BigInt 零              │
│ ""  ''  ``   │ 空字符串(所有引号形式)   │
│ null         │ 空值                   │
│ undefined    │ 未定义                 │
│ NaN          │ 非数字                 │
│ document.all │ ← 浏览器专属、HTML 规范   │
│              │   为兼容旧代码添加的特殊 falsy │
└──────────────┴──────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

容易误判为 falsy 但其实是 truthy 的值:

// 这些都是 truthy!
!![]           // → true(空数组是 truthy)
!!{}           // → true(空对象是 truthy)
!!'false'      // → true(非空字符串都是 truthy)
!!'0'          // → true
!!new Boolean(false)  // → true(Boolean 包装对象!)
!!' '          // → true(空格字符是非空字符串)
1
2
3
4
5
6
7

document.all 是唯一不是规范定义的 falsy 值——它是浏览器为兼容 if (document.all) 这类旧代码而特意做的例外。TC39 提案 IsHTMLDDA(host-defined falsy)允许宿主环境定义额外的 falsy 值。


# 6. 等号全推导

# 6.1 ECMA-262

疑惑:== 到底是怎么判断"抽象相等"的?

规范 ECMA-262 §7.2.15 定义了 Abstract Equality Comparison x == y。下面是逐条拆解(已按调用频率和重要性排序):

规则 1(同类型快速通道):
  If Type(x) === Type(y) → return x === y
  例: 5 == 5 → true; "a" == "a" → true

规则 2(null 和 undefined 互相豁免):
  If x is null && y is undefined → true
  If x is undefined && y is null → true
  例: null == undefined → true
      null == 0       → false  (不触发规则 2!因为 0 不是 undefined)

规则 3(Number vs String):
  If Type(x) is Number && Type(y) is String
  → return x == ToNumber(y)
  例: 5 == "5" → 5 == 5 → true

规则 4(String vs Number —— 同规则 3 的对称版本):
  同上,x 和 y 互换

规则 5(BigInt vs String):
  If Type(x) is BigInt && Type(y) is String
  → let n = StringToBigInt(y)
  → if n is NaN → false; else x == n
  例: 1n == "1" → 1n == 1n → true

规则 6(Boolean vs 任何 —— Boolean 先转 Number):
  If Type(x) is Boolean → return ToNumber(x) == y
  例: true == 1  → 1 == 1  → true
      true == "1" → 1 == "1" → 1 == 1 → true
      false == [] → 0 == ToPrimitive([])→0 → 0==0 → true ← 反直觉!

规则 7(String vs Symbol):
  If Type(x) is String or Number or BigInt or Symbol && Type(y) is Object
  → return x == ToPrimitive(y)
  例: "1" == [1]      → "1" == ToPrimitive([1]) → "1" == "1" → true
      42 == {valueOf(){return 42}} → 42 == 42 → true

规则 8(Object vs String/Number/BigInt/Symbol —— 同规则 7 的对称版本)

规则 9(BigInt vs Number —— 不允许!):
  If Type(x) is BigInt && Type(y) is Number(或反过来)
  → 如果 x 或 y 是 NaN 或 ±Infinity → false
  → 如果 x 的数学值 === y 的数学值 → true
  → 否则 false
  ⚠ 1n == 1 → true; 1n == 1.5 → false
    但 BigInt 和 Number 的 "==" 被允许——BigInt 和 Number 不能混用其他运算符(+ - *)

规则 10(Symbol 严格限制):
  If Type(x) is Symbol → return false(除非同类型——已由规则 1 处理)
  → Symbol() == Symbol()  → false(两个 Symbol 是不同的值)

规则 11(BigInt vs Undefined/Null):
  规范中没有特殊规则 → 走通用路径 → false
  → 0n == undefined → false
  → 0n == null → false

兜底规则: return false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 6.2 名人堂级反直觉案

把最有名的反直觉 == 案例逐个推演:

表达式 结果 推导链路
[] == ![] true ![]→false → []==0 → ""==0 → 0==0
[] == 0 true ToPrimitive([])=="" → ""==0 → 0==0
[] == "" true 同类型→[]不是""…不对——ToPrimitive([])="" → ""==""→true
[0] == false true false→0 → ToPrimitive([0])="0" → "0"==0 → 0==0
[1] == true true true→1 → [1]→"1" → "1"==1 → 1==1
[2] == true false true→1 → [2]→"2" → "2"==1 → 2==1→false
"" == false true false→0 → ""==0 → 0==0
"" == 0 true ""→0 → 0==0
"\t\n" == 0 true "\t\n"→Trim→""→0 → 0==0
null == 0 false null 不触发规则 2(因为 0 不是 undefined)→ 兜底→false
null == undefined true 规则 2:null ↔ undefined 豁免

# 6.3 === 为什么比

疑惑:=== 是不是永远比 == 快?

论证:=== 的算法比 == 简单得多——只需一步:

=== (Strict Equality Comparison) 的算法:
  1) 如果 Type(x) !== Type(y) → return false(直接拒绝!)
  2) 如果 Type(x) 是 Number:
     a) 如果 x 是 NaN 或 y 是 NaN → return false
     b) 如果 x 和 y 是同一个数字值 → return true
        (+0 和 -0 被认为是同一个值!)
     c) 否则 false
  3) 同类型非数字 → 按 SameValueNonNumber 比较
1
2
3
4
5
6
7
8

从规范角度看,=== 确实"更短"——它不需要 ToPrimitive/ToNumber/ToString 的任何转换。但实际上:

// 当两者类型相同且已经是基本类型时——
// === 和 == 走的是完全相同的 V8 内部路径(因为规则 1 直接跳到严格比较)
const a = 42, b = 42;
a === b;  // 类型相同 → 直接逐位比较
a == b;   // 类型相同 → 规则 1 → 跳到 === → 直接逐位比较
// ↑ 两者完全一样的机器码

// 当类型不同时——
'5' === 5;  // 类型不同 → 直接 false(1 条指令)
'5' == 5;   // 类型不同 → ToNumber → Trim → 比较(5+ 条指令)
// ↑ === 显著更快

// 当 "5" 已经 ToNumber 缓存在 Feedback Vector 里时——
// 热代码中,V8 可能已经把 == 编译为类似 === 的快速路径
// 所以在实际生产代码中,=== 和 == 的速度差异远小于理论分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

结论:在热代码中(被 TurboFan 优化后),== 和 === 的速度差异可以忽略不计。但在冷代码或类型不确定的路径上,=== 确实更简洁。=== 的真正优势不是性能,而是"你看代码时不需要在脑子里跑一遍 ToPrimitive 算法"——可读性才是 === 相比 == 最大的胜利。


# 7. 浮点精度详解

# 7.1 双精度 64 位

疑惑:JS 只有一种数字类型(Number),它在底层是怎么存的?

JS 的 Number 严格遵循 IEEE 754 双精度 64 位浮点数:

IEEE 754 双精度 64 位位布局:
┌──────┬─────────────────────────┬──────────────────────────────┐
│ Bit 63│ Bits 62~52 (11 bits)   │ Bits 51~0 (52 bits)          │
│ 符号位 │ 指数 (exponent)          │ 尾数/有效数 (mantissa/fraction)│
│ S     │ E (biased by 1023)      │ M (implicit leading 1)       │
└──────┴─────────────────────────┴──────────────────────────────┘

值 = (-1)^S × (1 + M/2^52) × 2^(E - 1023)

几个关键值在 64 位中的表示:
  0:      S=0, E=00000000000, M=全部0  →  0.0
  -0:     S=1, E=00000000000, M=全部0  → -0.0  (⚠ 0 === -0 → true)
  NaN:    S=0/1, E=11111111111, M ≠ 0  → NaN  (⚠ NaN !== NaN → true)
  Infinity: S=0, E=11111111111, M=0   → +∞
  -Infinity: S=1, E=11111111111, M=0  → -∞
  1.0:    S=0, E=01111111111(1023), M=0  → 1.0
  2.0:    S=0, E=10000000000(1024), M=0  → 2.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.2 0.1+0.2

疑惑:0.1 + 0.2 为什么是 0.30000000000000004 而不是 0.3?

论证:根因在于 0.1 和 0.2 在二进制中是无限循环小数——类似于十进制中 1/3 = 0.333... 永远写不完。

0.1 转二进制的过程:
  0.1 × 2 = 0.2 → 0
  0.2 × 2 = 0.4 → 0
  0.4 × 2 = 0.8 → 0
  0.8 × 2 = 1.6 → 1
  0.6 × 2 = 1.2 → 1
  0.2 × 2 = 0.4 → 0   ← 循环开始!
  0.4 × 2 = 0.8 → 0
  ...

0.1₁₀ = 0.00011001100110011001100110011...₂  (无限循环 0011)

IEEE 754 双精度只有 52 位尾数:
  0.1 被截断为 53 位的近似值(含隐式的 leading 1)
  0.2 同样被截断

  0.1 ≈ 1.1001100110011001100110011001100110011001100110011010 × 2^(-4)
  0.2 ≈ 1.1001100110011001100110011001100110011001100110011010 × 2^(-3)

  相加时需要对齐指数 → 右移 → 最后一位被舍入
  → 结果 = 0.3000000000000000444089209850062616...
  → JS 打印时只显示 17 位 → "0.30000000000000004"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

更可怕的例子——这会产生连锁效应:

// 金融计算中,这些浮点误差会逐步累积
let total = 0;
for (let i = 0; i < 10; i++) total += 0.1;
console.log(total);  // → 0.9999999999999999  不是 1.0 !
console.log(total === 1.0);  // → false
1
2
3
4
5

# 7.3 四种解决方案与各

方案 代码 适用场景 代价
整数化 (0.1*10 + 0.2*10) / 10 已知小数位数固定 只能处理整数范围的运算
toFixed (0.1+0.2).toFixed(1) 显示用(UI 输出) 结果是字符串,不能继续运算
Number.EPSILON Math.abs(a-b) < Number.EPSILON 通用比较 只适用于比较,不适用于累积运算
Decimal / BigInt 0.1n + 0.2n ❌ BigInt 不支持小数 → 用库(decimal.js) 金融/精确运算 引入外部依赖,运行速度慢 ~100 倍

通用浮点比较函数:

function approxEqual(a, b, epsilon = Number.EPSILON) {
  return Math.abs(a - b) < epsilon;
}
approxEqual(0.1 + 0.2, 0.3);  // → true
// 注意:epsilon 只适用于绝对值在 1 附近的数
// 对于极大或极小的数值,需要按比例缩放 epsilon
1
2
3
4
5
6

# 8. BigInt深度

# 8.1 Smi vs H

疑惑:第 02 篇讲了 V8 的隐藏类和对象布局——数字在 V8 内部是怎么存的?

论证:V8 的数字有三种表示,按取值范围和精度分工:

V8 数值表示的三层架构:
┌─────────────────────────────────────────────────────┐
│                                                     │
│  Smi(Small Integer,小整数):                         │
│    → 31 位有符号整数(64 位系统)                       │
│    → 范围:-2^30 ~ 2^30-1                            │
│    → 例:1, 42, -100                                │
│    → 存在栈上(指针标签位标识),无堆分配               │
│    → 最快:一次 ALU 操作完成加减                       │
│                                                     │
│  HeapNumber(堆数字):                                │
│    → 当数字超出 Smi 范围,或需要浮点精度时            │
│    → 在堆上分配一个 64 位 IEEE 754 双精度值           │
│    → 例:3.14, 2^53, NaN, Infinity                 │
│    → 有堆分配 + GC 开销                              │
│                                                     │
│  BigInt:                                            │
│    → 任意精度整数(理论上无限位)                      │
│    → 在堆上分配,位数可动态增长                       │
│    → 例:123456789012345678901234567890n           │
│    → 存储为符号位 + 多个 64 位"数字"组成的数组        │
│    → 最慢:每次运算需要在"数字数组"上做多精度运算       │
│                                                     │
└─────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Number 的安全边界:

// Number 的安全整数范围(由 IEEE 754 的 52 位尾数决定)
console.log(Number.MAX_SAFE_INTEGER);  // → 9007199254740991  (2^53 - 1)
console.log(Number.MIN_SAFE_INTEGER);  // → -9007199254740991

// 超过安全边界的后果——丢失精度:
console.log(9999999999999999);         // → 10000000000000000 ← 变成了另一个数!
console.log(9999999999999999 === 10000000000000000);  // → true!

// 为什么?
// 52 位尾数最多精确表示 2^53 = 9007199254740992 个整数
// 9007199254740992 ~ 18014398509481984 之间只有"偶数"能被精确表示
// 超过 18014398509481984 之后,间隔变成 4、8、16...
// 换句说:超过 2^53 后,不是所有整数都有自己的 IEEE 754 表示
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.2 BigInt

ES2020 引入 BigInt 后,刻意不提供 BigInt 和 Number 之间的隐式转换——因为这会丢精度:

// ❌ 不允许的混合运算
1n + 1;       // → TypeError: Cannot mix BigInt and other types
Math.max(1n, 2n);  // → TypeError(Math 函数需要 Number)
JSON.stringify(1n);  // → TypeError(JSON 不支持 BigInt)

// ✅ 允许的混合比较
1n == 1;      // → true(== 允许,§6.1 的规则 9)
1n === 1;     // → false(=== 不允许跨类型)
1n < 2;       // → true
1n > 0.5;     // → true

// ✅ 显式转换
BigInt(42);    // → 42n
Number(42n);   // → 42(可能丢失精度!)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.3 数值操作的安全边

操作 Smi(Number) BigInt
精确范围 -2^53 ~ 2^53 无限
支持小数 ✅ ❌(整数除自动截断→3n/2n=1n)
位运算 视为 32 位 无限位(~0n 返回负数补码)
与 JSON 互转 ✅ 原生支持 ❌ TypeError
与 + 运算符 ✅ ❌(不能与 Number 混合)
性能 最快 (Smi → ALU) 最慢 (多精度数组运算)

# 9. Intl 国际化

# 9.1 DateTime

Intl API 是 ECMA-402 规范的一部分——独立于 ECMA-262,存在于所有现代浏览器和 Node.js(依赖 ICU 库):

// DateTimeFormat:日期格式化
const formatter = new Intl.DateTimeFormat('zh-CN', {
  year: 'numeric', month: 'long', day: 'numeric',
  weekday: 'long', hour: '2-digit', minute: '2-digit'
});
console.log(formatter.format(new Date()));
// → "2026年6月12日星期五 15:30"

// NumberFormat:数字本地化
new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' })
  .format(12345.67);  // → "¥12,345.67"

new Intl.NumberFormat('de-DE').format(12345.67);  // → "12.345,67"(德国用逗号做小数点)

// RelativeTimeFormat:相对时间
new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' })
  .format(-3, 'day');  // → "3天前"
new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' })
  .format(1, 'week');  // → "下周"(auto 模式下用"下周"而非"1周后")

// ListFormat:列表连接
new Intl.ListFormat('zh-CN', { type: 'conjunction' })
  .format(['苹果', '香蕉', '橘子']);  // → "苹果、香蕉和橘子"
new Intl.ListFormat('en', { type: 'disjunction' })
  .format(['apple', 'banana', 'orange']);  // → "apple, banana, or orange"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 9.2 ICU 库依赖与

疑惑:Intl 凭什么能处理 400+ 种语言的格式差异?

答案在于它背后依赖的 ICU(International Components for Unicode)——一个由 Unicode 联盟维护的 C/C++ 库,包含所有语言的所有格式化规则。V8 打包了一个裁剪版的 ICU(icudtl.dat)。

数字系统的文化差异实例:

// 同一数字在不同语言中的格式完全不同
const num = 1234567.89;

// 阿拉伯数字(西方)→ "1,234,567.89"
new Intl.NumberFormat('en-US').format(num);

// 印度数字分组 → "12,34,567.89"(前三位一组,后面每两位一组)
new Intl.NumberFormat('en-IN').format(num);

// 阿拉伯语用不同的数字字形 → "١٬٢٣٤٬٥٦٧٫٨٩"
new Intl.NumberFormat('ar-EG').format(num);

// 中文大写数字 → "一百二十三万四千五百六十七点八九"
// (需要额外库,Intl 原生不直接支持中文大写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 10. 综合案例串讲

# 10.1 案例真相揭晓

逐一回答 §1 的六个疑问:

① typeof 遗产:typeof null === 'object' 源于 1995 年 JS 引擎用全零位表示 null,恰好与 Object 类型标签(000)重合。修复这个 bug 会破坏 Web 上已有的大量依赖——"bug 已成标准"。

② ToPrimitive:{} + 1 在不同上下文中结果不同——{} 可能被解析为代码块(得 1)或对象(得 "[object Object]1")。关键在 ToPrimitive 的 hint 参数和括号是否强制表达式上下文。Date 是唯一 hint="default" 时先调 toString 的对象。

③ ToNumber:Number(" ") 返回 0 因为 StringToNumber 在解析前先 Trim 首尾空格。空字符串→0。falsy 总共只有 8 个值。

④ == 全算法:[] == ![] 经过五步推导:![]→false → Boolean→ToNumber→0 → Object→ToPrimitive→"" → String→ToNumber→0 → 0==0→true。每一步都有规可依。

⑤ 浮点精度:0.1+0.2 !== 0.3 —— 因为 0.1 和 0.2 在二进制中是无限循环小数,IEEE 754 的 52 位尾数不足以精确表示。比较时用 Number.EPSILON 或整数化。

⑥ BigInt:9999999999999999 被 Number 截断成 10000000000000000 因为超出 53 位精确范围。BigInt 可以存任意精度的整数,但性能代价更大,且与 Number 不能隐式转换。

# 10.2 一个能处理 Na

把本文的所有类型判断细节集成到一个生产级 deepEqual 中:

function deepEqual(a, b, visited = new WeakMap()) {
  // 1) 同引用 → 直接 true(含 NaN 特判)
  if (Object.is(a, b)) return true;
  // Object.is 比 === 更精确:
  //   Object.is(NaN, NaN) → true  (=== 返回 false)
  //   Object.is(+0, -0)   → false (=== 返回 true)

  // 2) 类型不同 → false
  if (typeof a !== typeof b) return false;

  // 3) 非对象(基本类型 + function + symbol)→ 值比较
  if (typeof a !== 'object' || a === null || b === null) {
    // NaN 已在步骤 1 通过 Object.is 处理
    // -0 已通过 Object.is 处理
    // BigInt: typeof BigInt === 'bigint'(被 typeof 分辨,走值比较)
    // Symbol: typeof Symbol === 'symbol'(走值比较)
    return false; // 如果到这里 typeof 相同但 Object.is 不同 → false
  }

  // 4) 循环引用检测
  if (visited.has(a)) {
    // a 已经在这个 deepEqual 调用链中见过 → 循环引用
    return visited.get(a) === b; // b 是否就是同一个对象?
  }
  visited.set(a, b);

  // 5) 数组
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!deepEqual(a[i], b[i], visited)) return false;
    }
    return true;
  }

  // 6) 正则
  if (a instanceof RegExp && b instanceof RegExp) {
    return a.source === b.source && a.flags === b.flags;
  }

  // 7) Date
  if (a instanceof Date && b instanceof Date) {
    return a.getTime() === b.getTime();
  }

  // 8) Map
  if (a instanceof Map && b instanceof Map) {
    if (a.size !== b.size) return false;
    for (const [key, val] of a) {
      if (!b.has(key) || !deepEqual(val, b.get(key), visited)) return false;
    }
    return true;
  }

  // 9) Set
  if (a instanceof Set && b instanceof Set) {
    if (a.size !== b.size) return false;
    const arrA = [...a], arrB = [...b];
    // Set 无序 → 排序后比较
    arrA.sort(); arrB.sort();
    return deepEqual(arrA, arrB, visited);
  }

  // 10) 普通对象 / 类实例
  const keysA = Object.keys(a), keysB = Object.keys(b);
  if (keysA.length !== keysB.length) return false;
  for (const key of keysA) {
    if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
    if (!deepEqual(a[key], b[key], visited)) return false;
  }
  return true;
}

// 验证所有边缘情况:
console.log(deepEqual(NaN, NaN));               // → true  ✅
console.log(deepEqual(+0, -0));                 // → false ✅
console.log(deepEqual(1n, 1n));                 // → true  ✅
console.log(deepEqual(/abc/g, /abc/g));          // → true  ✅
console.log(deepEqual(new Date(0), new Date(0)));// → true  ✅

// 循环引用
const cyclicA = {}; cyclicA.self = cyclicA;
const cyclicB = {}; cyclicB.self = cyclicB;
console.log(deepEqual(cyclicA, cyclicB));        // → true  ✅(WeakMap 防止无限递归)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

# 10.3 设计哲学回扣

从类型系统和隐式转换中提炼三条设计哲学:

哲学一·「便捷优于正确」——JS 类型转换的设计原罪与挣扎

JS 诞生于"让非程序员也能写网页"的愿景之下。隐式类型转换(==、+ 的字符串拼接、ToBoolean)是这种愿景的直接产物——它们让一行 if (arr.length) 代替了 if (arr.length > 0),但也制造了 [] == ![] 这类经典陷阱。Brendan Eich 后来公开承认这是设计上的过度妥协。ES6+ 的 Symbol.toPrimitive、BigInt 的严格互操作限制、=== 的推广——都是 JS 在"为过去的便捷买单"的修正过程中。类型系统演进的本质不是"消除隐式转换",而是"给隐式转换加上清晰、可预测的规则"。

哲学二·「表示决定行为」——底层位布局塑造了上层语法

typeof null 的 bug、0.1+0.2 的精度问题、Smi/HeapNumber/BigInt 的三层表示,都与"这个值在内存中占据几位、怎么编码"直接相关。JS 不像 C 语言那样让你直接操作位——但它底层仍然受 IEEE 754、指针标签位、物理内存对齐的约束。理解类型系统不能只看 ECMA 规范,还要看到它跑在物理硬件上的真实表示。

哲学三·「容错性和精确性并存的紧张」——== vs === 的真正教训

== 和 === 的争论常常被简化为"永远用 ==="。但规范允许 ==(它不是 bug),给 == 定下了 15 条精确规则。真正的教训不是"== 是坏的"——而是隐式转换带来的认知负担: 看 x == y 时必须在脑子里跑一遍 ToPrimitive → ToNumber 链才能确定结果。=== 的优势不在于"更快"(实际差异微小),而在于"不需要额外的脑内推导"。一个好的类型系统设计应当让"看一眼就知道会发生什么"——而不是"看一眼规范才知道发生了什么"。

# 10.4 速查表:==

ToPrimitive 调用顺序决策树:

对象 → 有 [Symbol.toPrimitive]?
   ├─ 是 → 调用 fn(hint) → 须返回基本类型
   └─ 否 → hint 是什么?
           ├─ "number" → valueOf() → 基本类型? → 返回
           │              ↓ 非基本类型
           │            toString() → 基本类型? → 返回
           │              ↓ 非基本类型 → TypeError
           ├─ "string" → toString() → valueOf()
           └─ "default" → 同 "number"(Date 除外 → 同 "string")
1
2
3
4
5
6
7
8
9

Falsy 八值速查:false, 0, -0, 0n, "", null, undefined, NaN(+ 浏览器 document.all)

Number 安全边界:

边界 值 说明
MAX_SAFE_INTEGER 9007199254740991 (2^53 - 1) 最后一个"相邻相差 1"的整数
MIN_SAFE_INTEGER -9007199254740991
MAX_VALUE ~1.7977e+308 超过就是 Infinity
MIN_VALUE ~5e-324 最小的正非零数
EPSILON ~2.22e-16 1 和"比 1 大的最小浮点数"之差

下一步:类型系统通了。但真正让 JS 独一无二的是——执行上下文和闭包。为什么循环里的 var 是所有人踩过的坑?V8 怎么用 Context 链存闭包?进入 04.作用域链与闭包深度。

上次更新: 2026/06/16, 12:36:20
隐藏类与回收机制
作用域链闭包原理

← 隐藏类与回收机制 作用域链闭包原理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式