编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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 一个 JSON
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构全景概览
          • 2.1 proto
          • 2.2 为什么 JS 选
        • 3. 属性查找算法
          • 3.1 [[Get]]
          • 3.2 V8 隐藏类
          • 3.3 hasOwnPr
        • 4. new底层步骤
          • 4.1 四步拆解 + 手
          • 4.2 构造函数返回对象
          • 4.3 new.targ
        • 5. 五种继承演进
          • 5.1 原型链继承 →
          • 5.2 为什么寄生组合是
        • 6. class脱糖
          • 6.1 _classCa
          • 6.2 为什么 clas
          • 6.3 class 字段
        • 7. super原理
          • 7.1 [[HomeOb
          • 7.2 construc
          • 7.3 多重继承下 su
        • 8. 属性描述符详解
          • 8.1 数据描述符 vs
          • 8.2 freeze
          • 8.3 getter
        • 9. Mixin与安全
          • 9.1 对象混入 vs
          • 9.2 原型污染攻击
          • 9.3 Object.c
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 实现 Vue-s
          • 10.3 设计哲学回扣
          • 10.4 速查表
      • 代理与元编程协议
      • 事件循环承诺机制
      • 工作线程并发调度
      • 页面渲染像素原理
      • 网络接口存储架构
      • 服务端运行时编程
      • 模块系统双轨操作
      • 现代工程链三件套
      • 设计模式函数哲学
      • 跨端架构终局总结
  • CodeX
  • JavaScript入门
  • 专栏博客
杨充
2026-06-11
目录

原型链语法糖本质

# 06.原型链与类语法糖本质

📍 上接第 05 篇《this 四规则与函数组合》。函数的行为已了然。本文回到 JS 最核心的支柱——对象系统和原型链。{} 为什么能调用 .toString()?new 干了哪四件事?class 在 V8 里被脱糖成了什么?对象属性到底被藏在了哪里?

# 目录介绍

  • 1. 案例与疑问引入
    • 1.1 一个 JSON
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构全景概览
    • 2.1 proto
    • 2.2 为什么 JS 选
  • 3. 属性查找算法
    • 3.1 [[Get]]
    • 3.2 V8 隐藏类
    • 3.3 hasOwnPr
  • 4. new底层步骤
    • 4.1 四步拆解 + 手
    • 4.2 构造函数返回对象
    • 4.3 new.targ
  • 5. 五种继承演进
    • 5.1 原型链继承 →
    • 5.2 为什么寄生组合是
  • 6. class脱糖
    • 6.1 _classCa
    • 6.2 为什么 clas
    • 6.3 class 字段
  • 7. super原理
    • [7.1 [HomeOb
    • 7.2 construc
    • 7.3 多重继承下 su
  • 8. 属性描述符详解
    • 8.1 数据描述符 vs
    • 8.2 freeze
    • 8.3 getter
  • 9. Mixin与安全
    • 9.1 对象混入 vs
    • 9.2 原型污染攻击
    • 9.3 Object.c
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 实现 Vue-s
    • 10.3 设计哲学回扣
    • 10.4 速查表

# 1. 案例与疑问引入

# 1.1 一个 JSON

先看一段在生产环境真实出现过的代码——一个后台管理系统的"用户配置同步"功能:

// config-sync.js —— 用户配置同步器(故障版本)
async function syncUserConfig(userInput) {
  // 用户输入是一段 JSON——前端表单里"个性化设置"编辑器的产物
  let config = { theme: 'light', lang: 'zh' };

  // 合并用户自定义配置——允许用户覆盖默认值
  try {
    const userConfig = JSON.parse(userInput);  // ← 用户输入的 JSON

    // 浅合并:用户配置覆盖默认配置
    for (const key in userConfig) {
      config[key] = userConfig[key];  // ← 直接赋值——没有过滤!
    }
  } catch (e) {
    console.error('Invalid JSON');
    return;
  }

  // 把合并后的配置应用到页面
  applyConfig(config);
}

function applyConfig(config) {
  // 遍历所有配置项,应用到 DOM 和 localStorage
  for (const key in config) {
    if (config.hasOwnProperty(key)) {  // ← 用了 hasOwnProperty
      // ... 应用配置 ...
    }
    // ⚠️ 但如果 config.hasOwnProperty 被污染了呢?
  }
}
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

攻击载荷——一个恶意用户输入:

{
  "theme": "dark",
  "__proto__": {
    "hasOwnProperty": "polluted"
  }
}
1
2
3
4
5
6

现场:管理员保存这个配置后,整站所有页面变空白。控制台报错:Uncaught TypeError: config.hasOwnProperty is not a function。

根因:for (const key in userConfig) 遍历到了 __proto__ 这个属性(__proto__ 是 Object.prototype 上的一个可枚举的历史 getter/setter)。当执行 config['__proto__'] = userConfig['__proto__'] 时,它没有在 config 上创建新属性——而是把 Object.prototype.hasOwnProperty 替换成了字符串 "polluted"。此后,所有对象的 .hasOwnProperty() 都变成了 "polluted"() → TypeError。

# 1.2 顺藤摸到根因

带着这条线往下挖:

  • 假设 1:是不是 for...in 遍历了不应该遍历的属性?——是。for...in 会遍历对象自身的和原型链上的所有可枚举属性。__proto__ 虽然是历史遗留的 getter,但它可枚举、且存在于 Object.prototype 上——所以 for...in 会遍历到它。
  • 假设 2:为什么 config[key] = userConfig[key] 没有在 config 上创建一个新属性?——因为 __proto__ 不是普通的属性。它是 Object.prototype 上的一个 accessor property (getter/setter)。当你通过 obj.__proto__ = value 赋值时,它的 setter 不是去创建一个名为 __proto__ 的自身属性(own property)——而是去修改 obj 的内部 [[Prototype]] 槽。而这个赋值最终落到了 Object.prototype 上——因为 config 的原型是 Object.prototype(普通字面量创的)。
  • 假设 3:到底谁被污染了?——config 的 [[Prototype]] 是 Object.prototype。config.__proto__ = { hasOwnProperty: "polluted" } → 把 config 的原型从 Object.prototype 替换成了攻击输入的这个对象(从而让 config 自己失去 hasOwnProperty)——等等,更精确地说,这个赋值是修改 config.[[Prototype]],把它指向了 { hasOwnProperty: "polluted" }。然后 for...in 在遍历 Object.assign 时又把 __proto__ 上的值扩散了。
  • 假设 4:那为什么"所有对象"都被污染了?——实际上这个特定攻击只污染了用 Object.prototype 作为原型链顶端的对象。但如果你不小心把这个污染扩散到了 Object.prototype 自身(通过其他路径),那么所有 {} 都会受影响。

# 1.3 我们要回答什么

这段代码里至少藏着 7 个原理点:

① prototype / __proto__ / [[Prototype]] 三角到底什么关系?为什么有三个名字?  → 第 2 章
② 属性查找时 V8 怎么沿原型链找?为什么 1 万次 obj.toString 不会每次都遍历?      → 第 3 章
③ new 到底做了什么?手写一个不到 15 行的 myNew——它必须处理什么边界条件?        → 第 4 章
④ 五种继承方式:原型链→盗用→组合→寄生组合→class——每一步修了上一步的什么bug?  → 第 5 章
⑤ class 脱糖后是什么?Babel 的 _classCallCheck 是怎么实现"不准直接调用"的?   → 第 6 章
⑥ super.method() 怎么知道"父类"是谁?[[HomeObject]] 是个什么东西?          → 第 7 章
⑦ configurable/writable/freeze/seal 怎么联动?原型污染攻击怎么防?           → 第 8~9 章
1
2
3
4
5
6
7

本篇路线:

架构总图(第 2 章)
   ↓
属性查找(第 3 章)──→ 解开"obj.toString 到底从哪来、V8 怎么加速它"
   ↓
new 四步骤(第 4 章)──→ 解开"构造函数到实例的精确映射"
   ↓
五种继承(第 5 章)──→ 解开"每一步在解决上一步的什么问题"
   ↓
class 脱糖(第 6 章)──→ 解开"class 在 V8 里的真实面目"
   ↓
super 本质(第 7 章)──→ 解开"[[HomeObject]] 如何锁定父类"
   ↓
属性描述符(第 8 章)──→ 解开"对象的元数据层"
   ↓
Mixin 与安全(第 9 章)──→ 解开"CVE-2018-3721 是怎么发生的"
   ↓
综合案例(第 10 章)──→ 案例彻底剖开 + 哲学四条 + 速查表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

📌 本篇定位:这是 JS 对象系统的地基篇。第 04 篇讲了作用域和闭包(变量在哪里),第 05 篇讲了 this 和函数(调用者是谁)。本篇回答:对象是什么、它身上的属性和方法从哪来、prototype 和 class 之间隔了什么。读完本篇后,你对 {} 不再有"它从哪里来、要到哪里去"的模糊感。

# 2. 架构全景概览

# 2.1 proto

疑惑:同一件事——"对象的原型"——为什么有三个名字?它们是不是同一个东西的不同叫法?

论证——它们是三个不同层次的概念:

┌────────────────────────────────────────────────────────────────────┐
│             原型链三个名字的层次关系                                   │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  [[Prototype]](ECMA-262 规范层)                                     │
│  ┌──────────────────────────────────────────────────────────┐      │
│  │  ECMA-262 规定的每个 JS 对象的内部槽(Internal Slot)。        │      │
│  │  每个对象都有一个 [[Prototype]] 指向另一个对象或 null。          │      │
│  │  JS 代码不能直接读/写这个槽——只能通过辅助 API。                   │      │
│  │  规范访问方式:                                               │      │
│  │    Object.getPrototypeOf(obj) → 读                            │      │
│  │    Object.setPrototypeOf(obj, proto) → 写                     │      │
│  │  V8 中:存储为对象的第一个字段(Map 偏移量中的 prototype 指针)    │      │
│  └──────────────────────────────────────────────────────────┘      │
│                          ↕ 实现                                        │
│  __proto__(历史遗留 getter/setter)                                  │
│  ┌──────────────────────────────────────────────────────────┐      │
│  │  定义在 Object.prototype 上的 accessor property (get/set)。     │      │
│  │  它的 getter 内部调用:Object.getPrototypeOf(this)             │      │
│  │  它的 setter 内部调用:Object.setPrototypeOf(this, value)       │      │
│  │  规范标记为 [[Legacy]]——不推荐在生产代码中使用                     │      │
│  │  ⚠️ 可以被覆盖或删除(因为它是普通属性,在 Object.prototype 上)    │      │
│  └──────────────────────────────────────────────────────────┘      │
│                          ↕ 模板                                        │
│  prototype(构造函数属性)                                             │
│  ┌──────────────────────────────────────────────────────────┐      │
│  │  只有函数对象(包括 class)有这个属性(箭头函数没有!)               │      │
│  │  函数声明时自动创建:Fn.prototype = { constructor: Fn }         │      │
│  │  它是一个**普通的对象**——只是被 new 操作符用来设置新对象的原型        │      │
│  │  在 new Fn() 时:newObj.[[Prototype]] = Fn.prototype         │      │
│  └──────────────────────────────────────────────────────────┘      │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘
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

三个名字的关系(以 function Dog() {} 为例):

Dog 函数本身:
  Dog ──prototype──→ Dog.prototype
                        ├ constructor ──→ Dog
                        └ [[Prototype]] ──→ Object.prototype(因为 Dog.prototype 是字面量 {})

Dog 实例:
  const d = new Dog()
  d ──[[Prototype]]──→ Dog.prototype
  d.__proto__ === Dog.prototype  ← 规范上等价(但不推荐用 __proto__)
1
2
3
4
5
6
7
8
9

V8 内部——[[Prototype]] 的存储位置:

在 V8 的 HeapObject 实现中,[[Prototype]] 不是独立的槽——它存储在对象的 Map(隐藏类) 的 prototype 字段中。所有共享同一个 Map 的对象,它们的原型一定相同(因为原型是 Map 的字段之一)。这就是为什么"改一个对象的原型"代价很高——V8 必须给它分配一个新的 Map。

# 2.2 为什么 JS 选

疑惑:几乎所有主流语言(Java/C++/C#/Python)都用 class 做继承。为什么 JS 选择了一个"原型链"——这个东西在 1995 年几乎是"另一种道路"?

论证——Brendan Eich 在 2001 年的采访中解释了设计决策:

  1. 时间紧迫——JS 在 10 天内设计完成(1995 年 5 月)。实现一个完整的 class 系统(包括静态类型检查、多重继承、虚函数表等)在当时不可能做到。原型链是最简化的"有继承能力的对象系统"——只需要两个核心概念:对象和对象之间的单向指针。

  2. "足够好"——JS 不是要替代 Java——Netscape 的市场策略是"JS 是 Java 的脚本小助手"。不需要一个完整的 OOP 系统。原型链足够让开发者"把方法挂到一个共享对象上",节省内存。

  3. Self 语言的影响——Brendan Eich 的灵感来源是 Self(Sun Microsystem 的 Smalltalk 变体)——它是第一种用"原型"替代"类"的主流语言。Self 证明了没有 class 也能实现继承、多态和代码复用——原型链就是 Self 的简化版。

  4. 动态友好的设计——原型链天然支持运行时修改类的行为:

// 在 Java 中给所有 String 加一个方法 → 不可能(除非改 JDK 源码)
// 在 JS 中 → 改原型即可
Array.prototype.last = function() { return this[this.length - 1]; };
[1, 2, 3].last();  // → 3 ← 所有数组立即拥有了 .last()!
1
2
3
4

这种"运行时扩展"在 Class 系统中需要"猴子补丁"(monkeypatching),在原型链中是天生的。但这也是原型污染攻击的根基——你能加好方法,攻击者也能加坏方法。

结论:JS 选择原型链不是"技术选型的偏好",而是**"用一个最简单的概念实现'有继承能力的对象系统'"的务实决策**。25 年后,ES6 的 class 语法糖证明了:原型链的底层能力完全足够支撑类式 OOP——它只是差了一层语法糖。

# 3. 属性查找算法

# 3.1 [[Get]]

ECMA-262 §10.1.8 规定了 obj.prop 的查找流程:

┌──────────────────────────────────────────────────────┐
│  [[Get]](P, Receiver) —— 属性 P 的完整查找流程          │
├──────────────────────────────────────────────────────┤
│                                                      │
│  ① 调用 [[GetOwnProperty]](P)                          │
│     → 在 obj 自身的属性中查找 P                         │
│     → 找到 → 把属性描述符转成值 → 返回                  │
│     → 没找到 → 进入步骤 ②                             │
│                                                      │
│  ② 获取 parent = obj.[[Prototype]]                    │
│     → parent === null → 返回 undefined(链顶端)       │
│     → parent 是普通对象 → 递归:parent.[[Get]](P, Receiver)│
│     → parent 是 Proxy → 调用 proxy 的 get trap       │
│                                                      │
│  ③ 如果找到的属性是 accessor property (getter/setter): │
│     → 调用 getter.call(Receiver)                     │
│     → 注意:this 是 Receiver(最初发起查找的那个对象!)│
│                                                      │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

一个容易被忽略的细节——Receiver 的作用:

const parent = {
  get name() {
    return this._name;  // ← this 是谁?
  }
};
const child = Object.create(parent);
child._name = 'child';

console.log(child.name);  // → 'child' ← this 是 child(Receiver),不是 parent!
// 因为 [[Get]] 的第 3 步传了 Receiver = child(发起查找的那个对象)
// getter 里的 this 绑定到 Receiver
1
2
3
4
5
6
7
8
9
10
11

# 3.2 V8 隐藏类

疑惑:原型链查找听起来是 O(链长) 的——每次 obj.toString 都要沿着 obj → prototype → Object.prototype 找三次。V8 怎么能让 10000 次 obj.toString 都在 ~1µs 内完成?

论证——V8 的 IC 不"每次遍历",而是"第一次遍历后记住路径":

┌──────────────────────────────────────────────────────────┐
│    V8 内联缓存(IC)在原型链查找中的应用                      │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  第一次执行 obj.toString:                                 │
│  ┌────────────────────────────────────────────────────┐  │
│  │ ① 读 obj 的 Map(隐藏类) → MapA                      │  │
│  │ ② 在 MapA 的 descriptor array 中找 'toString'        │  │
│  │    → 没找到                                         │  │
│  │ ③ 读 MapA.prototype → Object.prototype 的 Map        │  │
│  │ ④ 在 Object.prototype 的 descriptor array 中找到     │  │
│  │    'toString' 在 offset 28                           │  │
│  │ ⑤ 记录到 IC:{ MapA, "toString", path: [offset 28],  │  │
│  │                prototype_chain: 1 hop } ← 记下来!  │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  第二次及以后(IC 命中):                                  │
│  ┌────────────────────────────────────────────────────┐  │
│  │ ① 读 obj 的 Map → MapA ✓(和第一次一样)               │  │
│  │ ② IC 命中了!→ 直接读 [obj + prototype_offset + 28]   │  │
│  │    不遍历、不查 descriptor array、不比较属性名          │  │
│  │    → ~1-2 CPU 指令(一次指针偏移 + 一次内存读)         │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  TurboFan JIT 更进一步——直接内联为:                        │
│    mov rax, [obj + 8]      ; 读__proto__                │
│    mov rax, [rax + 28]     ; 读 toString 在原型上的偏移   │
│    → 2 条 mov 指令 = ~1ns(CPU cache 命中时)             │
│                                                          │
└──────────────────────────────────────────────────────────┘
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

IC 失效的条件——什么时候"路径"作废:

const obj = { x: 1 };
obj.toString();  // 第一次:IC 记住了原型查找路径

// 场景 1:改变了 obj 的原型 → IC 失效!
Object.setPrototypeOf(obj, null);
obj.toString();  // → TypeError!IC 存的路径错了(原型变成 null 了)

// 场景 2:obj 的类型变了(隐藏类不同)→ IC 失配
// V8 会记录多个隐藏类的 IC 路径,达到上限(通常 4 个)后降级为常规查找
1
2
3
4
5
6
7
8
9

# 3.3 hasOwnPr

操作 查找范围 安全风险 ES 版本
obj.hasOwnProperty('x') 仅自身属性 ⚠️ 可以被覆盖(obj.hasOwnProperty = 'xxx') ES3
'x' in obj 自身 + 整条原型链 ✅ in 是操作符,不能被覆盖 ES3
Object.hasOwn(obj, 'x') 仅自身属性 ✅ 静态方法,不受原型链影响 ES2022
// 危险——hasOwnProperty 被覆盖后
const obj = { hasOwnProperty: 'nope', x: 1 };
obj.hasOwnProperty('x');           // → TypeError: not a function

// 安全的替代
Object.prototype.hasOwnProperty.call(obj, 'x');  // → true(老方案)
Object.hasOwn(obj, 'x');                          // → true(ES2022 最佳实践)
1
2
3
4
5
6
7

# 4. new底层步骤

# 4.1 四步拆解 + 手

疑惑:new Fn() 到底做了什么?ECMA-262 规定了哪四步?

论证——ECMA-262 §13.2.2 [[Construct]] 的四步展开:

function myNew(Constructor, ...args) {
  // 步骤 ①:创建一个新的空对象
  const obj = {};  // 规范:OrdinaryObjectCreate

  // 步骤 ②:设置这个对象的 [[Prototype]] = Constructor.prototype
  Object.setPrototypeOf(obj, Constructor.prototype);
  // 等价于:obj.__proto__ = Constructor.prototype

  // 步骤 ③:执行构造函数,this = obj
  const result = Constructor.apply(obj, args);

  // 步骤 ④:如果构造函数返回了对象(不是原始类型)→ 返回那个对象
  //         否则 → 返回步骤 ① 创建的对象
  return result instanceof Object ? result : obj;
}

// 验证:
function Dog(name) { this.name = name; }
const d = myNew(Dog, '旺财');
console.log(d.name);                  // → '旺财'
console.log(d instanceof Dog);        // → true
console.log(d.__proto__ === Dog.prototype); // → true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 4.2 构造函数返回对象

// 返回对象 → new 返回那个对象(不是 this!)
function Foo() {
  this.name = 'foo';
  return { override: true };
}
const f = new Foo();
console.log(f.name);     // → undefined ← 不是 'foo'!
console.log(f.override);  // → true ← 返回的是 return 的那个对象

// 返回基本类型 → new 忽略它,返回 this
function Bar() {
  this.name = 'bar';
  return 42;  // ← 基本类型 → 被忽略
}
const b = new Bar();
console.log(b.name);  // → 'bar' ← this 照常返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4.3 new.targ

// new.target:当前 [[Construct]] 调用中的构造函数
function Person(name) {
  if (!new.target) {
    // 如果是普通调用(Person() 而不是 new Person())→ new.target = undefined
    throw new Error('Person must be called with new');
  }
  this.name = name;
}

// instanceof:检查 Constructor.prototype 是否在 obj 的原型链上
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 5. 五种继承演进

# 5.1 原型链继承 →

┌─────────────────────────────────────────────────────────────────┐
│                五种继承方式的精确进化链                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ① 原型链继承(最早)                                              │
│     做法:Child.prototype = new Parent()                         │
│     优点:父类方法被所有子实例共享(节省内存)                        │
│     问题:父类的引用类型属性也被所有子实例共享 ← 改一个全改!          │
│                                                                 │
│  ② 盗用构造函数继承                                               │
│     做法:在 Child 内部 Parent.call(this)                        │
│     优点:每个实例有独立的属性                                      │
│     问题:无法继承父类原型上的方法(eat 这些在 Parent.prototype 上)   │
│                                                                 │
│  ③ 组合继承(原型链 + 盗用构造)                                   │
│     做法:① + ② 组合                                            │
│     优点:方法继承 + 属性独立                                     │
│     问题:Parent 被调用了两次(new Parent() + Parent.call(this))  │
│                                                                 │
│  ④ 寄生组合继承(最优——ES6 class 的底层蓝图)                      │
│     做法:Child.prototype = Object.create(Parent.prototype)       │
│           + Parent.call(this)                                    │
│     优点:方法继承 + 属性独立 + Parent 只被调用一次 ✅               │
│     本质:用 Object.create 代替 new Parent()——不调用父构造器         │
│                                                                 │
│  ⑤ ES6 class extends(语法糖——底层是 ④)                         │
│     做了额外的优化:方法默认不可枚举、强制 super() 必须先调           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
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

代码对照:

// ① 原型链继承——引用共享 bug
function Parent() { this.items = []; }
function Child() {}
Child.prototype = new Parent();
const c1 = new Child(); c1.items.push('a');
const c2 = new Child(); console.log(c2.items);  // → ['a'] ← 共享了!

// ② 盗用构造——方法不继承
function Child() { Parent.call(this); }
// c1.eat() ← 不存在!eat 在 Parent.prototype 上

// ④ 寄生组合——完美方案
function inherit(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype);
  Child.prototype.constructor = Child;
}
function Child() { Parent.call(this); }
inherit(Child, Parent);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 5.2 为什么寄生组合是

寄生组合解决了所有前置版本的问题,而没有引入新的概念——它的两个核心操作(Object.create + Parent.call(this))都是 ES5 的原生能力。ES6 class 脱糖后正是调用这两个操作完成继承。

# 6. class脱糖

# 6.1 _classCa

// 源码
class Person {
  constructor(name) { this.name = name; }
  greet() { return `Hi ${this.name}`; }
}

// Babel 脱糖产物(精简后逐行注释):
var Person = /*#__PURE__*/(function () {
  // ① 构造函数本体——和 function Person(name) {} 没区别
  function Person(name) {
    _classCallCheck(this, Person);  // ← 防"忘记 new"
    this.name = name;
  }

  // ② 挂原型方法——把 greet 挂到 Person.prototype 上
  _createClass(Person, [{
    key: "greet",
    value: function greet() { return "Hi " + this.name; }
  }]);

  return Person;
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

三个辅助函数的逐行拆解:

// _classCallCheck —— 防"忘记 new"
function _classCallCheck(instance, Constructor) {
  // instanceof 检查:instance 的原型链上有 Constructor.prototype 吗?
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
// Person() → 进入函数 → instance = undefined → !(undefined instanceof Person) → throw
// new Person() → instance = {}(新创建的对象)→ 对象的原型链上有 Person.prototype → 通过

// _createClass —— 把方法数组挂到原型上(支持不可枚举!)
function _createClass(Constructor, protoProps) {
  for (var i = 0; i < protoProps.length; i++) {
    Object.defineProperty(Constructor.prototype, protoProps[i].key, {
      value: protoProps[i].value,
      enumerable: false,   // ← class 方法默认不可枚举(区别于 ES5 原型方法!)
      writable: true,
      configurable: true
    });
  }
}

// _inherits —— 寄生组合继承
function _inherits(Child, Parent) {
  Child.prototype = Object.create(Parent.prototype);  // ← 只继承原型方法
  Child.prototype.constructor = Child;
  Object.setPrototypeOf(Child, Parent);  // ← 让子类能访问父类的静态方法
}
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

# 6.2 为什么 clas

new Foo();  // → 正常!function 整体提升
function Foo() {}

new Bar();  // → ReferenceError: Cannot access 'Bar' before initialization
class Bar {}
1
2
3
4
5

class 声明的行为和 let/const 完全一致——创建阶段绑定进入 TDZ(第 04 篇 §5.1)。为什么?因为 class 的构造函数体中可能访问到自身或父类的静态属性——如果允许在声明前使用,就会读到"还在 TDZ 中"的变量→更难排查的 bug。

# 6.3 class 字段

class Base {
  field = 'base';        // ← class field(public instance field)
  constructor() {
    console.log('Base constructor');
    console.log(this.field);  // → 'base'(field 在 super 后、constructor 代码前被初始化)
  }
}

class Child extends Base {
  field = 'child';
  constructor() {
    super();              // ← 1. 调用父构造器(Base.constructor 执行)
    console.log(this.field);  // → 'child'(子类 field 在 super 后初始化)
  }
}
new Child();
// 输出顺序:
//   Base constructor
//   base           ← Base.constructor 里的 this.field
//   child          ← Child.constructor 里的 this.field
//
// 字段初始化顺序:
//   super() → 父类 field 初始化 → 子类 field 初始化 → constructor 剩余代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 7. super原理

# 7.1 [[HomeOb

疑惑:super.method() 怎么知道"父类"是谁?类内部没有显式存"父类"的变量啊。

论证——每个方法(不是每个函数——是"类方法"这种简写语法定义的方法)内部隐藏了一个 [[HomeObject]] 槽:

class Dog extends Animal {
  speak() {
    super.eat();  // ← super 怎么找"父类"?
  }
}

// 关键:Dog.prototype.speak 在创建时,[[HomeObject]] 被设为 Dog.prototype
// speak.[[HomeObject]] = Dog.prototype

// super.eat() 的查找路径:
// ① 从 speak 中拿到 [[HomeObject]] = Dog.prototype
// ② 取 Dog.prototype 的原型链上一级 → Object.getPrototypeOf(Dog.prototype)
//    = Animal.prototype(因为在 class extends 中设置了 Dog.prototype.__proto__ = Animal.prototype)
// ③ 在 Animal.prototype 上找 'eat' → 找到!
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为什么 super 不能在普通函数中使用:

const obj = {
  method() {
    super.toString();  // ✅ 可以——method 的 [[HomeObject]] = obj
  }
};

function foo() {
  super.toString();  // ❌ SyntaxError——普通函数没有 [[HomeObject]]!
}
1
2
3
4
5
6
7
8
9

# 7.2 construc

class Child extends Parent {
  constructor() {
    super();              // ← super() = 调用父类构造器(必须是在 this 之前第一行!)
    // 等价于:Parent.call(this)——但这步由引擎保证,不能自己写
  }

  method() {
    super.method();       // ← super.method() = 沿 [[HomeObject]] 的原型链查找
    // 等价于:Parent.prototype.method.call(this)
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 7.3 多重继承下 su

JS 的原型链是单链——每个对象的 [[Prototype]] 只指向一个对象。这意味着 super 的查找不会"分叉"去找多个父类——这是 JS 不原生支持多重继承的根本原因。但 Mixin 模式(第 9 章)通过"把多个父类的方法拷贝到一个原型上",模拟了多重继承的效果。

# 8. 属性描述符详解

# 8.1 数据描述符 vs

每个对象属性的底层是一个 Property Descriptor(属性描述符)——一个包含六个位的元数据对象。不是所有位都同时存在——数据描述符和访问器描述符互斥:

┌──────────────────────────────────────────────────────┐
│           属性描述符的两种形态(互斥)                     │
├──────────────────────────────────────────────────────┤
│                                                      │
│  数据描述符(data descriptor)——属性存的是"值":           │
│  ┌──────────────────────────────────────────────┐    │
│  │ value       → 属性的当前值                     │    │
│  │ writable    → 是否可以修改 value               │    │
│  │ enumerable  → 是否出现在 for...in / keys 中   │    │
│  │ configurable → 是否可删除 / 可修改其他描述符位   │    │
│  └──────────────────────────────────────────────┘    │
│                                                      │
│  访问器描述符(accessor descriptor)——属性存的是"函数":   │
│  ┌──────────────────────────────────────────────┐    │
│  │ get         → 取值函数(读取属性时调用)          │    │
│  │ set         → 设值函数(写入属性时调用)          │    │
│  │ enumerable  → 同上                            │    │
│  │ configurable → 同上                            │    │
│  └──────────────────────────────────────────────┘    │
│                                                      │
│  ⚠️ 互斥规则:value 和 get 不能同时存在                   │
│      writable 和 get/set 不能同时存在                  │
│                                                      │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj = {};
Object.defineProperty(obj, 'x', {
  value: 42,
  writable: false,
  enumerable: true,
  configurable: false
});

obj.x = 100;       // 静默失败(严格模式下抛 TypeError)
delete obj.x;      // 静默失败——configurable: false 不可逆!

// ⚠️ configurable: false 一旦设置,就永远不能再改成 true!
//   也不能把属性从 data descriptor 改成 accessor descriptor
1
2
3
4
5
6
7
8
9
10
11
12
13

# 8.2 freeze

操作 等价于对每个属性设置 可加新属性 可删属性 可改已有属性值 可逆?
preventExtensions — ❌ ✅ ✅ ❌
seal configurable: false ❌ ❌ ✅(如果 writable: true) ❌
freeze configurable: false + writable: false ❌ ❌ ❌ ❌

关键细节——freeze 是浅冻结:

const obj = { a: { b: 1 } };
Object.freeze(obj);
obj.a.b = 2;       // ✅ 可以改!freeze 不冻结嵌套对象!
obj.a;  // → { b: 2 }
1
2
3
4

# 8.3 getter

// 场景 1:数据校验——拦截不合法赋值
const user = {
  _age: 0,
  get age() { return this._age; },
  set age(val) { if (val < 0) throw Error('age >= 0'); this._age = val; }
};

// 场景 2:惰性计算——第一次访问时算,之后用缓存
const stats = {
  _data: [1, 2, 3, 4, 5],
  get sum() {
    const val = this._data.reduce((a, b) => a + b, 0);
    Object.defineProperty(this, 'sum', { value: val, writable: true, configurable: true });
    return val;  // ← 以后 sum 变成普通数据属性——不再重复计算!
  }
};

// 场景 3:兼容旧 API——内部改成新实现,外部接口不变
const legacyAPI = {
  get fullName() { return `${this.firstName} ${this.lastName}`; }
  // 老代码还在读 fullName,但内部已经是合成属性
};

// 场景 4+5:响应式拦截——Vue 2.x 的 defineProperty 响应式(见 §10.2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 9. Mixin与安全

# 9.1 对象混入 vs

// 对象混入:浅拷贝属性——简单但容易覆盖已有属性
const mixin = (target, ...sources) => {
  for (const src of sources) {
    for (const key of Reflect.ownKeys(src)) {
      // 跳过原型污染风险键:__proto__ / constructor / prototype
      if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
      Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(src, key));
    }
  }
  return target;
};

// 类混入:利用 class extends 的链式表达能力
const Timestamped = Base => class extends Base {
  createdAt = Date.now();
};
const Serialized = Base => class extends Base {
  serialize() { return JSON.stringify(this); }
};
class Model extends Timestamped(Serialized(Object)) {}
const m = new Model();
console.log(m.createdAt);  // → 时间戳(来自 Timestamped)
console.log(m.serialize()); // → JSON 字符串(来自 Serialized)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 9.2 原型污染攻击

2018 年 lodash 的 defaultsDeep 函数被爆出一个原型污染漏洞(CVE-2018-3721)。攻击源头是——当用户可控的数据被用来做 _.defaultsDeep 合并时,__proto__ 这个键被当作"普通属性"而非"原型链访问器"处理:

// 攻击载荷
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');

// 如果合并函数没过滤 __proto__:
// _.defaultsDeep({}, payload)
// → Object.prototype.isAdmin = true ← 污染了全局原型!

// 之后:
const user = {};  // 普通用户对象
console.log(user.isAdmin);  // → true ← 原型被污染!
1
2
3
4
5
6
7
8
9
10

防护措施:

// ① 用 Object.create(null) 创建无原型的"纯字典"
const safeObj = Object.create(null);
// safeObj.__proto__ = xxx → 在 safeObj 上创建一个叫做 "__proto__" 的普通属性
// 不会影响原型链——因为 safeObj 没有原型链!

// ② 过滤危险键
const BLOCKED_KEYS = ['__proto__', 'constructor', 'prototype'];
function safeAssign(target, source) {
  for (const key of Object.keys(source)) {
    if (BLOCKED_KEYS.includes(key)) {
      console.warn(`Blocked prototype pollution attempt: ${key}`);
      continue;
    }
    target[key] = source[key];
  }
}

// ③ 冻结 Object.prototype(防御终极手段——但可能影响第三方库)
Object.freeze(Object.prototype);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 9.3 Object.c

用 Object.create(null) 用普通 {}
✅ 纯字典(只做 KV 存储)——如缓存、配置、路由表 ✅ 需要 toString/hasOwnProperty 等原型方法
✅ 防御原型污染——__proto__ 变成普通属性 ✅ 需要放到原型链上被其他对象继承
✅ 不受第三方代码污染影响 ✅ 需要配合框架的响应式系统(Vue 2 通过原型链追踪变更)
// ❌ 不应该用 Object.create(null) 的地方
const vm = Object.create(null);
// Vue 不能把 vm 做成响应式——因为 Vue 2 依赖 Object.defineProperty 和原型链
// 第 10.2 节会展开
1
2
3
4

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章的原型污染攻击——七个疑问现在能逐条作答:

疑问 答案
① prototype / __proto__ / [[Prototype]] 的关系 第 2.1 节:[[Prototype]] 是规范内部槽(读用 getPrototypeOf);proto 是 Object.prototype 上的 legacy accessor;prototype 是函数的属性,作为 new 的原型模板。三个层次
② V8 怎么沿原型链找属性 第 3.2 节:第一次查找遍历原型链→IC 记录"Map + 原型跳数 + 属性偏移"→后续直接两条 mov 指令完成读取
③ new 的四步是什么 第 4.1 节:创建空对象→设置 [[Prototype]] → 执行构造器→返回对象/this
④ 五种继承方式每一步解决了什么 第 5 章:原型链(共享引用bug)→盗用构造(方法不继承)→组合(Parent 调两次)→寄生组合(一次+方法继承)→class(语法糖+不可枚举)
⑤ class 脱糖后是什么 第 6.1 节:_classCallCheck防忘记 new + _createClass 挂原型方法 + _inherits 寄生组合继承
⑥ [[HomeObject]] 怎么让 super 找到父类 第 7.1 节:每个类方法创建时记录 [[HomeObject]]=该方法所在的原型对象→super 沿这个对象的原型链查找
⑦ configurable/writable/freeze 怎么联动 第 8 章:configurable: false 不可逆→seal = 所有属性设 configurable: false + 禁加属性→freeze = seal + 所有数据属性 writable: false

修复方案(按代价从小到大):

方案 A:for...in 换 Object.keys + 过滤危险键

// ❌ 危险——for...in 遍历原型链
for (const key in userConfig) { config[key] = userConfig[key]; }

// ✅ 安全——只遍历自身属性 + 过滤 __proto__
for (const key of Object.keys(userConfig)) {
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
  config[key] = userConfig[key];
}
1
2
3
4
5
6
7
8

方案 B:用 Object.create(null) 创建目标对象(防御终极方案)

// config 没有原型链——__proto__ 此时只是一个普通字符串属性
const config = Object.create(null);
// config.__proto__ = xxx → 在 config 上创建名为 "__proto__" 的普通数据属性
// 不影响任何原型链!
1
2
3
4

# 10.2 实现 Vue-s

// ── defineProperty 版(Vue 2.x 风格)──
let activeEffect = null;  // 当前正在执行的依赖收集函数
const targetMap = new WeakMap();

function reactive(obj) {
  for (const key of Object.keys(obj)) {
    defineReactive(obj, key, obj[key]);
  }
  return obj;
}

function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  if (typeof val === 'object' && val !== null) reactive(val);

  const dep = new Set();  // 依赖集合(订阅这个 key 的所有 effect)
  Object.defineProperty(obj, key, {
    get() {
      if (activeEffect) dep.add(activeEffect);  // 收集依赖
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      if (typeof newVal === 'object') reactive(newVal);  // 新值是对象→递归
      dep.forEach(effect => effect());  // 通知更新
    },
    enumerable: true,
    configurable: true
  });
}

// 缺陷:
//  1. 无法检测新增属性(obj.newKey = 'val' 不会触发 get/set)
//  2. 无法检测删除属性(delete obj.key 不会触发 set)
//  3. 数组索引赋值(arr[0] = x)和 .length 修改无法检测
//  4. 递归遍历整个对象——大对象初始化慢

// ── Proxy 版(Vue 3.x 风格——在 07 篇全面展开)──
const reactiveMap = new WeakMap();  // 缓存:同一个对象只创建一次 Proxy

function reactive(obj) {
  if (reactiveMap.has(obj)) return reactiveMap.get(obj);

  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);  // ← 可拦截动态新增属性!
      const res = Reflect.get(target, key, receiver);
      return typeof res === 'object' && res !== null ? reactive(res) : res;  // 懒递归
    },
    set(target, key, value, receiver) {
      const old = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (old !== value) trigger(target, key);  // ← 可拦截新增和修改
      return result;
    },
    deleteProperty(target, key) {
      const had = key in target;
      const result = Reflect.deleteProperty(target, key);
      if (had) trigger(target, key);  // ← 可拦截删除!
      return result;
    }
  });

  reactiveMap.set(obj, proxy);
  return proxy;
}

function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  if (activeEffect) dep.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) dep.forEach(effect => effect());
}
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

# 10.3 设计哲学回扣

哲学一·「委托优于复制——原型链是单一共享、链式查找的哲学」

JS 没有选择"每个实例拷贝一份方法"(浪费内存),而是让所有实例共享同一份原型上的方法,通过原型链查找。这是对"大部分方法在所有实例上都是一样的"这一观察的极致利用——用一条单向链表省掉了 n 份拷贝。但这种"委托"机制的代价是:属性访问的性能依赖于链的长度(除非 V8 的 IC 记住了路径)。这也是为什么 ES6 class 选择在原型上定义方法而非在 constructor 中定义——前者是共享的,后者是拷贝的。

哲学二·「class 不是新类型——是原型链的语法封装,JS 的对象模型从 1995 年至今没有变过」

ES6 的 class 没有引入新的对象模型。它只是对寄生组合继承 + 原型链的语法封装。class A {} 在 V8 内部的表示和 function A() {} 没有任何本质区别——两者都被编译为同一个 JSFunction C++ 对象。JS 的 class 是"糖"不是"新基础设施"——这既是它的简洁所在(不用学两套对象模型),也是"为什么 JS 的 class 和 Java 的 class 不一样"的根源(没有静态类型系统、没有接口、没有重载)。

哲学三·「属性描述符是 JS 的'反射元数据'层——JS 在运行时给你 C++ 编译器级别的控制力」

configurable / writable / enumerable / get / set——这套元数据体系让 JS 的对象模型具有传统 OOP 语言少见的"运行时自省"能力。你可以冻结任何对象(freeze)、拦截任何属性的读写(getter/setter)、控制属性的可见性(enumerable)——这种元数据层是 Vue 响应式系统(defineProperty 版)的基石。Proxy 在 ES6 中把这个能力扩展到了 13 种操作(属性读写之外还包括 delete、in、Object.keys、构造函数调用等)——这是 JS 从"属性级别的拦截"到"对象级别的拦截"的一次跃迁。

哲学四·「原型污染是'动态扩展'的硬币反面——你能加好方法,攻击者也能加坏方法」

原型链的"运行时扩展"能力(如 Array.prototype.last = fn)是 JS 灵活性的象征。但它也是原型污染攻击的入口——因为"任何人"都可以往 Object.prototype 上加属性。这引出了 JS 安全的一个底层矛盾:"默认开放" vs "默认安全"。ES2022 引入 Object.hasOwn(静态版 hasOwnProperty)就是为了让开发者可以安全地检查自身属性而不受原型污染影响。Object.create(null) 提供了一个"关闭原型链"的逃生舱——用"放弃继承"换"绝对安全"。

# 10.4 速查表

三角模型速查:

名字 谁有这个属性 可读/可写 规范地位 用途
[[Prototype]] 每个对象 规范内部(JS 不可直接访问) ECMA-262 Internal Slot 构成原型链
__proto__ Object.prototype 上的 accessor ✅ 可读可写 [[Legacy]](不推荐) 历史遗留
prototype 只有函数(包括 class) ✅ 可读写 ECMA-262 §20.2.4 new 的原型模板

继承五法演进速查:

方式 属性独立 方法复用 父类构造器调用次数 缺陷
原型链继承 ❌(共享引用) ✅ 1 次 引用类型被所有实例共享
盗用构造函数 ✅ ❌ 1 次 方法不能复用
组合继承 ✅ ✅ 2 次 父类被重复调用
寄生组合继承 ✅ ✅ 1 次 无(ES6 class extends 的蓝图)
ES6 class extends ✅ ✅ + 不可枚举 1 次(super()保证) 无

三级冻结速查:

方法 禁止添加新属性 禁止删除属性 禁止修改已有属性值 可逆?
preventExtensions ✅ ❌ ❌ ❌
seal ✅ ✅ ❌(writable: true 可改) ❌
freeze ✅ ✅ ✅(数据属性全设 writable: false) ❌

class 脱糖对照速查:

class 语法 脱糖等价 防护了什么
class Foo {} function Foo() { _classCallCheck(this, Foo); } 防止直接调用(Foo() 报错)
method() {} Object.defineProperty(Foo.prototype, 'method', { value: fn, enumerable: false }) 方法不可枚举
class Child extends Parent {} _inherits(Child, Parent) = Object.create + Object.setPrototypeOf 寄生组合继承 + 静态方法继承
constructor() { super(); } Parent.call(this) + 强制先 super 确保 this 初始化链完整

下一步:属性描述符有局限——检测不到新增和删除的属性。ES6 给了我们一个更强大的工具——Proxy 能拦截 13 种操作。进入 07.Proxy 元编程与迭代协议。

上次更新: 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
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式