对象、原型与类 — 属性背后的机关,和「找不到就往上问」的继承
对象、原型与类 — 属性背后的机关,和「找不到就往上问」的继承
这一章讲三件事: 对象的属性远比「键值对」复杂——能只读、能隐藏、能挂钩子(getter/setter)、 能被 Proxy 整体拦截;原型——JavaScript 的继承真相:读一个属性找不到, 就沿着一条「后备链」一路问上去;以及 class——ES2015 的语法糖,糖衣底下仍是构造函数加原型, 唯一真正的新东西是
#私有字段。读完你会明白:「class 只是语法糖」不是一句修辞,而是可验证的机制描述。
1. 先看现象:一个属性,远不止「键和值」
const ninja = { skillLevel: 100 };最普通的对象。但马上出现三种普通属性解决不了的需求:1
- 想禁止直接改 skillLevel(只能经批准的方法改);
- 想读取时实时计算(skillLevel 由别的因素推导);
- 想在被改时触发动作(记日志、做校验)。
这三件事同一个解法:getter 和 setter——一对特殊方法,让「读属性」「写属性」这两个动作 在执行时悄悄改道去调用函数。先分清楚两个常被混用的意思:2
- 按约定叫的 getter/setter:普通方法起名
getFirstNinja()、setFirstNinja(x), 命名只是风格,语言不关心; - 语言特性 get/set:方法伪装成属性,读
obj.firstNinja时函数在幕后执行。
本章讲的当然是后者。
主线例子:一个带钩子的忍者名册
全章前半段用它:一个名册对象,firstNinja 不是数据,而是「读时取数组第一项、写时改数组第一项」的
计算属性。带数走一遍(书里的 Listing 9.1):3
const ninjaCollection = {
ninjas: ["Yoshi", "Kuma", "Hattori"],
get firstNinja() { // get 定义读钩子
console.log(`Getting firstNinja: ${this.ninjas[0]}`);
return this.ninjas[0];
},
set firstNinja(newNinja: string) { // set 定义写钩子
console.log(`Setting firstNinja: ${newNinja}`);
this.ninjas[0] = newNinja;
},
};
console.assert(ninjaCollection.firstNinja === "Yoshi", "…"); // ① 触发 get
ninjaCollection.firstNinja = "Hachi"; // ② 触发 set
console.assert(ninjaCollection.firstNinja === "Hachi"
&& ninjaCollection.ninjas[0] === "Hachi", "…"); // ③ 数据真的变了运行输出三行日志,顺序揭露机制:
Getting firstNinja: Yoshi ← ① 读属性,get 方法执行,返回 ninjas[0]
Setting firstNinja: Hachi ← ② 写属性,set 方法执行,ninjas[0] 被改
Getting firstNinja: Hachi ← ③ 再读,拿到的是新值看起来的属性访问,实际上是两次方法调用——这就是「方法伪装成值」的全部机关。
(firstNinja() 这样当函数调反而会报错:它是属性,不是方法。)
已存在的对象想补 getter/setter,用 Object.defineProperty(见 §2);get/set 关键字
只在新建对象字面量或类时能用。4
原书点出这个特性的行业意义:JavaScript API 可以用「像普通属性一样直接读写」的接口,
内部却藏着校验、日志、内部表示转换——Java 里那一族 getX()/setX() 样板代码,
在这里一行都不用写。5
2. Object.defineProperty:属性的完整「说明书」
用 Object.defineProperty(对象, 属性名, 描述符) 可以给已有对象定义(或重定义)属性。
描述符(descriptor)是一份说明书,分两个家族。
2.1 一个出乎意料的差异:Object.keys 看不见它
先做实验(书里的)。上面 §1 用 get/set 关键字定义的对象,和用 defineProperty 定义的等价物,
分别过一遍 Object.keys:6
// 字面量 get/set 版
Object.keys(ninjaCollection) // [ 'ninjas', 'firstNinja' ] ← firstNinja 在列
// defineProperty 版
Object.keys(ninjaCollection) // [ 'ninjas' ] ← firstNinja 「消失」了!同一个属性,一个可数一个不可数——区别在描述符的开关上。
defineProperty 创建的属性,默认不可枚举(enumerable: false)。
不可枚举意味着它被这些机制集体无视:Object.keys、Object.values、Object.entries、
for...in、JSON.stringify、对象展开/assign 拷贝。7
这个机制不新鲜:每个对象字面量自带 toString 方法(继承自 Object 原型,§3),
但 Object.keys 从不见它——它就是不可枚举的。让属性可枚举,描述符里加
enumerable: true 即可。一个常用模式由此而来:数据属性可枚举、方法不可枚举,
消费者就能用 const {...data} = obj 一把拷走全部数据,不带上方法。8
2.2 访问器描述符(accessor descriptor):get/set 加 enumerable、configurable 两个选项
描述符里出现 get 或 set(或都有),就叫访问器描述符。开关共四个:9
| 开关 | 默认 | 管什么 |
|---|---|---|
get / set | 无 | 读/写时执行的函数 |
enumerable | false | 上面那串枚举机制见不见它 |
configurable | false | 定义本身还能不能改(重定义/delete) |
只给 get 不给 set = 只读属性:写入时报错(严格模式)或静默失败(非严格——又一个用 strict 的理由)。 只给 set 不给 get = 只写属性:读出来恒为 undefined。书里「密码」的例子很传神:10
const obj = { passwordHash: "" };
Object.defineProperty(obj, "password", {
set(newPassword: string) { // 只有写钩子
this.passwordHash = crypto.hash("sha1", newPassword);
},
});
obj.password = "12345";
console.assert(obj.password === undefined, "The password is read-only"); // 读:undefined
console.assert(obj.passwordHash.length === 40, "sha1 produces a 40-char hash"); // 写:存的是哈希密码只能往里写、读永远 undefined,底层存的是 40 字符的 SHA-1 哈希——单向属性的教科书实现。
configurable: false 的定义是永久的:再 defineProperty 覆盖它、或 delete 它,都抛错。
设成 true 则允许覆盖和删除。11
2.3 数据描述符(data descriptor):没有函数的值
只想存个值,但要求「不可枚举」或「只读」?那是数据描述符的领地:value + 三个开关
(enumerable/configurable 同上,外加 writable,默认 false——即默认只读)。12
const obj = {};
Object.defineProperty(obj, "surfNinjaCount", { value: 3 });
obj.surfNinjaCount = 4; // 严格模式 TypeError:不可写「non-configurable + non-writable」= 事实上的永久常量;「writable 但 non-configurable」= 值能改、性质不能改(比如不能把数据属性改成 getter)。13
3. Proxy:不只拦一个属性,而是拦「整个对象」
getter/setter 管一个属性;Proxy(代理) 管一个对象的全部交互——读、写、删、枚举、 方法调用,统统能被拦下自定义。原书的定位:Proxy 是 getter/setter 的推广。14
结构两件套:
new Proxy(target, handler):target 是被代理的原对象;handler 是一组陷阱(traps), 每个陷阱对应一类操作(get、set、deleteProperty、ownKeys、defineProperty……);Reflect:Proxy 的配套工具箱,把「默认操作」做成函数 (Reflect.get(target, key, receiver)≈target[key],receiver 是 this 应指的对象)。
机制一句话:对 proxy 做任何操作,引擎先看 handler 里有没有对应陷阱;有,交给陷阱裁决; 没有,直通原对象。(Figure 9.2 画的就是这个岔路。)15
书里「天皇与代理人」的例子把 get/set 陷阱走了一遍:16
const emperor = { name: "Komei" };
const representative = new Proxy(emperor, {
get: (target, key, receiver) => {
return key in target
? Reflect.get(target, key, receiver) // 有的属性:正常透传
: "Don't bother the emperor!"; // 没有的:代理人挡驾
},
set: (target, key, value, receiver) => {
if (!(key in target)) {
console.warn("Setting this key is impermissible.");
return false; // 拒绝这次写入
}
return Reflect.set(target, key, value, receiver);
},
});
representative.name; // "Komei" —— 透传
representative.name = "Tenno";
emperor.name; // "Tenno" —— 写入直达 target
representative.nickname; // "Don't bother the emperor!" —— get 陷阱挡驾
representative.nickname = "x"; // 警告 + 拒绝 —— set 陷阱挡驾带数走:读 name → 陷阱查 name in emperor 为真 → Reflect.get 透传 → "Komei";
写 name → 陷阱放行 → emperor 的 name 真被改了(proxy 是通道,不是拷贝);
读 nickname → in 为假 → 返回挡驾字符串。注意:没有陷阱介入的操作(比如 in 本身、JSON 化)
走默认路径。17
Proxy 的实用坐标(书里给了三个):18
- 调试与性能调优:get 陷阱可以给对象的所有方法自动包上计时日志——前提是架构上留好了 「关键对象都经工厂函数产出」的口子,才好在工厂里换成 proxy 版;
- 测试框架的自动 mock:Vitest 的
vi.mock(..., { spy: true })能把模块的所有导出 换成间谍函数,底层就是 Proxy; - 语言级黑科技:Sindre Sorhus 的
negative-array用 proxy 实现 Python 风格的负索引数组 (ES2022 的.at(-1)是它的官方缩水版)。
以及一句性能警告:引擎遇到 proxy 要做大量额外工作,性能关键路径别用。19
4. 原型:继承就是「找不到,就问下一个」
4.1 回退语义
JavaScript 的继承机制说穿了一句话:
读一个对象的属性,它自己身上没有,就去它的原型(prototype)上找; 原型上没有,再去原型的原型上找——这条「原型链(prototype chain)」一路问到底。20
注意方向只此一条:读有回退,写没有——赋值永远落在自己身上(已经有同名属性才覆盖)。
也注意 JavaScript 的继承不区分「数据」和「方法」,一切属性同等可继承。
(in 运算符测的是「整条链上可不可见」,不是「自己有没有」——后者用 Object.hasOwn。)
主线例子带数走(书里的 yoshi/hattori/kuma,本章后半程的承重件):21
const yoshi = { skulk: true };
const hattori = { sneak: true };
const kuma = { creep: true };
"sneak" in yoshi; // false —— 初始:yoshi 只会 skulk
Object.setPrototypeOf(yoshi, hattori);
"sneak" in yoshi; // true —— hattori 成为后备:yoshi「会」sneak 了
Object.setPrototypeOf(hattori, kuma);
"creep" in hattori; // true
"creep" in yoshi; // true —— 链式回退:yoshi → hattori → kuma三步的查找路径,最后一次 yoshi.creep 的解析是:
yoshi 自己有 creep 吗? 没有
↓ 问原型 hattori
hattori 有 creep 吗? 没有
↓ 问原型 kuma
kuma 有 creep 吗? 有 → 返回 trueObject.setPrototypeOf(a, b) 把 b 设为 a 的原型(原书用它在平级对象间演示;
生产代码应避免事后改链,引擎对「创建时就定好链」的对象有优化)。
链的尽头是 Object.prototype——所以 ({}).toString() 人人可用;
Object.create(null) 造没有原型的对象(纯字典的推荐姿势,第 11 章还有更好的:Map)。22
这套机制的动态性值得记三笔(原书列的):原型上的方法随时可加可改,一改全体后备对象生效
(早年 MooTools 改 Element.prototype 给 DOM 加功能,所谓 monkey patching,今天被视为破坏);
对象自己定义同名属性就遮蔽原型上的;运行中整条链可以换掉(慎用)。23
4.2 构造函数与 prototype:配对出厂
对象字面量之外,造对象的正统是 new 一个构造函数。第 6 章讲过 new 的三步;
本章补上最关键的一件事实:
new X()造出的对象,其原型是X.prototype。
每个 function 声明/表达式在创建时,自动获得一个空对象作为它的 prototype 属性;
用这个函数当构造器时,造出的所有实例都以这个空对象为原型。24
function Ninja() {} // 空函数
Ninja.prototype.swingSword = () => true; // 往它的 prototype 上加方法
const ninja = new Ninja();
ninja.swingSword(); // true —— 自己没有,原型上有
ninja instanceof Ninja; // true —— Ninja.prototype 在 ninja 的原型链上带数走:new Ninja() → 造空对象 obj → obj 的原型被设为 Ninja.prototype → 作为 this 进函数体
(空函数啥也不干)→ 返回 obj。ninja.swingSword():ninja 自己没有 → 原型上有 → 返回 true。
一个方法都不在实例上,却人人可调用——这就是继承省内存的方式:方法一份,实例共享。
(对照第 6 章「构造器里 this.skulk = function…」的写法:那样每个实例各拷一份函数,浪费。)
instanceof 的准确语义也由此定型:「X.prototype 在不在 obj 的原型链上」,
别和 typeof 混——typeof 任何实例 都是 "object"。25
彩蛋:ninja.constructor.name === "Ninja"(构造器属性指回函数,函数有名字属性)——调试小技巧,
生产慎用(改个函数名、过一遍压缩器,逻辑就断)。26
现在可以兑现本章标题里的判词了。原书明说:Listing 9.5 那套构造函数 + prototype 的写法,
在 post-ES2015 的世界里「显得没必要地绕」;而 class 做的「是同样的事」——
类,本身就是一个函数加一个原型对象。 这是「语法糖」三个字在此处的精确含义:没有新机制,
只有新语法。下一节看糖怎么化。27
5. class:糖衣,以及糖里唯一的新东西
5.1 类 = 构造函数 + 原型,一次写完
class Ninja {
name: string;
level: number;
constructor(name: string, level: number) { // constructor 是关键字,不是函数名
this.name = name; // 赋给实例的属性
this.level = level;
}
swingSword() { return true; } // 简写方法 → 挂在 Ninja.prototype 上
}
const ninja = new Ninja("Yoshi", 4);三个 assert 把「糖」字坐实(书里的):28
ninja instanceof Ninja—— 类是构造器(Ninja 本身能被 new);typeof Ninja.prototype.swingSword === "function"—— 实例方法住在原型对象上,不在实例上;Object.hasOwn(ninja, "name")—— constructor 里赋的属性才是实例自己的。
一张对照表看穿糖纸:
| class 语法 | 等价的老写法 |
|---|---|
constructor(...) {…} | 函数体本身 |
swingSword() {…}(类体内简写) | Ninja.prototype.swingSword = function… |
new Ninja(...) | 同左(类也必须 new,不能直接调) |
static compare(...) | Ninja.compare = function…(挂在函数上,不进原型) |
static(静态)成员挂在类(函数)自己身上,实例摸不到——适合放「与某个实例无关」的工具方法 (如两个忍者比级别)。static 属性不继承(子类要自己再写,见下节)。29
5.2 extends 与 super:原型链的声明式
class Person {
name: string;
constructor(name: string) { this.name = name; }
dance() { return true; }
}
class Ninja extends Person { // 声明链:Ninja 的原型链上接 Person
sneakiness: number;
constructor(name: string, sneakiness = 3) {
super(name); // 先跑父类构造器(同一个 this!)
this.sneakiness = sneakiness;
}
sneak() { return `Sneak level ${this.sneakiness}`; }
}实例化的查找链(书里的 Figure 9.7):ninja.sneak() 在 Ninja.prototype 上找到;
ninja.dance() 自己这一级没有,沿链回退到 Person.prototype 找到。
两个 instanceof 同时成立:ninja instanceof Ninja、ninja instanceof Person——
链上每一环都算数。30
super 的纪律:在子类构造器里,用 this 之前必须先调 super()——父类要先完成初始化, 同一个 this 才能用;不定义构造器则默认继承父类的。继承是单线的:一个类只能 extends 一个类, 没有多继承。31
5.3 # 私有字段:糖里唯一的新机制
ES2022 引入,只在 class 里可用(普通对象没有这门语法):32
class Ninja {
name: string;
static #ninjas: Ninja[] = []; // 静态私有字段
constructor(name: string) {
this.name = name;
Ninja.#ninjas.forEach((ninja) => ninja.#greet(name)); // 类体内可用
Ninja.#ninjas.push(this);
}
#greet(newNinjaName: string) { // 私有方法
console.log(`Greetings, ${newNinjaName}! I'm ${this.name}.`);
}
}
new Ninja("Kasumi"); // Kasumi 报到,无人迎接
new Ninja("Ryu"); // 「Greetings, Ryu! I'm Kasumi.」
new Ninja("Ken"); // 两位前辈轮流迎接三条铁律(书里的清单,逐条都是「撞墙即语法错」级别的硬):33
- 只能点号访问:
ninja.#greet行;ninja["#greet"]是 undefined(字符串键和私有键是两个世界); - 只能类体内访问:那对花括号之外写
#greet,解析期就报错——不是运行时检查,是语法层面封死; - 不能被代理、枚举、删除:Proxy 陷阱看不见私有键;Object.keys 永远列不出;delete 不允许。
为什么用「field(字段)」这个词而不用「property」:私有键不是普通属性, 上面三条特殊性每一条都超出属性描述符的能力范围。34
实用价值(原书给了两层):硬隔离(真的访问不到,不是约定)是少数场景的需求;
更日常的价值是人机工学——# 开头的名字一眼可见是内部实现,
IDE 也不会在外面自动补全它们。35
6. 作者的判断与证据
可亲手验证的机制(全章 assert 覆盖):
- defineProperty 的属性默认不可枚举(Object.keys 对照实验);
new X()的原型是X.prototype(swingSword 演示);- 类=函数的判定(instanceof + typeof prototype.swingSword);
- 私有键的访问限制(类体外引用是语法错,连运行都轮不到)。36
作者立场(当立场读):
- 「monkey patching 已不合时宜、class+TypeScript 更可预测」是行业主流判断,
但原书留了一手:单元测试里改
Date.prototype.getTimezoneOffset归一时区,仍是「有时是最好的选择」; - 「关键对象走工厂函数以便日后换 Proxy」是作者的架构建议,非通则;
- 「引擎对运行中改原型链的优化假设」是性能提示,无定量数据。
7. 边界与局限
Object.setPrototypeOf的生产禁令只给了一句话的理由(引擎优化假设), 细节要查引擎文档;__proto__这个旧 accessor 本章未提。- Proxy 陷阱的完整清单只点了五个名字,MDN 有全表;in 操作、
Object.keys、 原型访问各自走哪个陷阱,本章未细分。 - 类字段(ES2022
=语法) 在本章只以私有字段形式出现;公有字段与它的原型/实例之别, 散见第 6 章(类字段是特殊箭头函数)。 - 装饰器、抽象类、接口实现检查(TypeScript 的类扩展能力)不在本章,也不在 MEAP 任何已出章节。
8. 可带走的(合上这一章,你应该有这十条)
- get/set 让方法伪装成属性:读执行 get、写执行 set;只给 get = 只读,只给 set = 只写。
- defineProperty 默认不可枚举:Object.keys/JSON/展开都看不见;想要「数据可拷、方法不带」,方法设 non-enumerable。
- 描述符两家族:访问器(get/set + enumerable/configurable)管钩子,数据(value + 三开关)管裸值;writable 默认 false。
- Proxy 拦整个对象,陷阱按操作分派;Reflect 把默认操作做成函数——测试框架的自动 mock 底层就是它。
- 继承 = 读属性的链式回退:自己身上没有问原型,原型没有再问原型的原型;写永远落在自己身上。
new X()的原型 =X.prototype:方法放原型上(一份共享),数据 constructor 里赋(各实例自有)。instanceof测「X.prototype 在不在链上」,不是测类型;typeof对所有实例都只给 "object"。- class 是糖:constructor=函数体、简写方法=prototype 赋值、static=挂函数自身;底层仍是函数+原型。
- extends 把链接上,super 是同一个 this 上先跑父类构造器;用 this 前必须先 super;单继承。
#私有字段是唯一新机制:类体内、点号、语法级封死——不可代理、不可枚举、不可删。
10. 深读一:getter/setter 的完整机制,从「约定」到「语言特性」
10.1 两种「getter/setter」的分界线
第 1 节开头就把一对易混的概念掰开了,这里展开:按约定叫的 getX/setX 只是普通方法, 命名风格而已,语言不在乎;语言特性 get/set 则是方法伪装成属性—— 调用方读写的是属性语法,执行的是你的函数。把书里的两版并排,差别一目了然:1
// 约定版:调用方要记「这是方法」
ninjaCollection.getFirstNinja();
ninjaCollection.setFirstNinja("Hachi");
// 特性版:调用方按普通属性读写,钩子在后头执行
ninjaCollection.firstNinja; // 触发 get
ninjaCollection.firstNinja = "Hachi"; // 触发 set特性版的行业意义(书里点破):API 可以用「像普通属性一样直接读写」的接口形态,
内部却藏着校验、日志、内部表示转换——Java 里成对的 getX/setX 样板,在这门语言里
一行都不用写。代价是调试时要多想一层:obj.x 可能不是读字段,而是在跑函数。
10.2 Listing 9.2 的 TypeScript 曲折,一段小档案
用 defineProperty 给已有对象补属性时,书里的代码有一段刻意的 TypeScript 麻烦, 值得当档案存:
const ninjaCollection = {
ninjas: ["Yoshi", "Kuma", "Hattori"],
} as NinjaCollection; // ① as:断言它「将有」firstNinja
Object.defineProperty(ninjaCollection, "firstNinja", { get() {...}, set(...) {...} });麻烦的根源书里说得坦白:TypeScript 的变量类型是静态的、不是动态的——
同一个变量在程序的不同位置不能有不同的类型。要么让类型停在字面量那一步
(没有 firstNinja),要么用 as 提前断言它有(尽管 defineProperty 运行之前它真没有)。
书里选了后者,因为类型会在 defineProperty 之后变正确。
这是「动态特性 vs 静态类型」这一根本张力的一处缩影,不是 bug。2
defineProperty 的常见实战场景书里也点了:测试——在模拟 DOM 环境里永久覆盖 一个你不控制的对象上的值(比如 window.location),defineProperty 是那把钥匙。
11. 深读二:描述符的完整账本
11.1 访问器描述符的四个选项,逐个走样例
| 开关 | 默认 | 样例行为(书里的断言) |
|---|---|---|
get | 无 | 只给 get:读正常,写 TypeError(严格) |
set | 无 | 只给 set:写执行 set,读恒 undefined |
enumerable | false | false 时 Object.keys/JSON/展开全不见它 |
configurable | false | false 时 delete 和重定义都 TypeError |
「只给 set」的密码例子值得再读一遍——它是「写钩子有、读钩子无」的最实用形态:
const obj = { passwordHash: "" };
Object.defineProperty(obj, "password", {
set(newPassword: string) {
this.passwordHash = crypto.hash("sha1", newPassword); // 只存哈希
},
});
obj.password = "12345";
console.assert(obj.password === undefined, "The password is read-only");
console.assert(obj.passwordHash.length === 40, "sha1 produces a 40-char hash");密码只能往里写、读永远 undefined,底层是 40 字符的 SHA-1——单向属性的教科书实现。 顺带:非严格模式下写只读属性是静默失败——strict mode 又一次成了你的报警钟。3
configurable 的语义有一个「永久」的层次值得记牢:non-configurable + non-writable = 事实上常量;writable 但 non-configurable = 值能改、性质不能改(不能再变成 getter/setter,也不能改 enumerable)。4
11.2 数据描述符:什么时候值得用它
只想存个值,什么时候不用 obj.x = 3 而要用 defineProperty?书里的两个触发条件:
- 想让它不可枚举(Object.keys 看不见,比如内部状态字段);
- 想让它只读但没有 getter 的运行时开销(writable: false 由引擎直接拦, 不经过函数调用)。
数据描述符的默认值口诀(书里散落各处,合并):value 给什么是什么,三个开关
enumerable/configurable/writable 全都默认 false——所以裸写
Object.defineProperty(obj, "x", { value: 3 }) 得到的属性是:
可读、不可枚举、不可改、不可删。想要常见行为得自己开灯。5
11.3 Symbol 键的枚举特殊性
书里有一个边栏级的细则:Symbol 键的属性也能用 defineProperty 定义、
也能设 enumerable——但就算 enumerable 为 true,Object.keys 和 JSON.stringify
照样忽略它。唯一在乎 Symbol 属性 enumerable 的地方,是 Object.assign
和对象展开拷贝它的时候。所以「Symbol + 默认」就是一道天然的隐藏层:
故意不进入序列化产物的元数据,挂 Symbol 键上。6
12. 深读三:Proxy 的完整机制与陷阱清单
12.1 运作流程,逐步
对 proxy 做任何操作(读、写、删、枚举、调方法……)
│
▼
引擎查 handler:有对应陷阱吗?
│
├─ 有 ──► 陷阱函数接管,它的返回值/行为决定操作结果
│ (陷阱里通常用 Reflect 把默认行为转发给 target,再加自己的逻辑)
└─ 无 ──► 直通 target,如同 proxy 不存在Reflect 是这套机制的另一半:没有它,陷阱里想做「默认行为 + 一点附加」
就得手写默认实现;有了它,Reflect.get(target, key, receiver) 一行就把
「按默认规则读属性」转发出去,receiver 还保住了 this 的正确指向。
Proxy 管拦截、Reflect 管转发,两者是配套的。7
12.2 陷阱清单(书里点名的五个,加注释)
| 陷阱 | 拦截什么 | 典型用途 |
|---|---|---|
get | 读属性 | 默认值、日志、挡驾(书里的天皇代理人) |
set | 写属性 | 校验、只读拒绝 |
deleteProperty | delete 操作 | 禁止删除 |
ownKeys | Object.keys / for...in 的键列表 | 隐藏键、键排序 |
defineProperty | defineProperty 及「给新属性赋值」 | 冻结结构 |
MDN 有全表(书里给了链接);记住「每个对象操作都有对应陷阱」这个原则即可—— get/set 只是最常见的两个。8
12.3 三个真实用例的完整坐标
书里给了三个 Proxy 的实用坐标,逐个展开:
- 调试计时:get 陷阱可以为对象的每个方法包一层计时日志—— 「哪个方法被调了、耗时多久」。前提:关键对象都经工厂函数产出, 才好在工厂里「狸猫换太子」换成 proxy 版。这是架构层面的建议, 不是写完代码再想换就能换的;
- 测试框架的自动 mock:Vitest 的
vi.mock("./math.js", { spy: true })能把模块的所有导出换成间谍函数,背后就是 Proxy—— 你「感知的魔法」是拦截器机制的量产版; - 语言级黑科技:Sindre Sorhus 的 negative-array 用 proxy 实现 Python 风格的
负索引(
arr[-1]从尾部数)。ES2022 的.at(-1)是它的官方缩水版—— 库能抢先语言一步,正是 Proxy 这种通用机制的存在意义。9
一句性能警告(书里加粗):引擎处理 proxy 要做大量额外工作,性能关键路径别用—— 拦截的通用性是用运行时开销换的。10
13. 深读四:原型链的完整推导
13.1 「回退」语义的三个精确性质
书里的原型继承有三个性质,逐条都值得对着实验记:
- 只回退读:赋值永远落在对象自己身上;已有同名属性才被覆盖。原型上的同名 属性不会因为「你写了一个」而改变——它一直在,只是被遮蔽了;
- 数据与方法同等可继承:不像某些语言区分「字段继承」和「方法继承」, JavaScript 里一切属性都是键值对,一律可继承。书里的 yoshi/hattori/kuma 用的全是数据属性,照样构成继承演示;
- 链的深度不限:yoshi → hattori → kuma 是两层,链可以一直接到 Object.prototype 尽头。任何一次属性读,最差走完整条链。11
13.2 构造器与原型的配对,一句定理
把第 4 节的机制压成一句可引用的定理:
每个 function 声明/表达式在创建时,自动获得一个空对象作为它的
prototype; 用这个函数当构造器造出的所有实例,都以这个空对象为原型。
推论:ninja instanceof Ninja 的真值条件是「Ninja.prototype 在 ninja 的原型链上」
——所以 instanceof 不是类型检查,是链成员检查。而 typeof ninja 只给
"object",连类名都不肯透露。想知道对象出自哪个类?ninja.constructor.name 是个
调试技巧(构造器属性指回函数,函数有名字属性),但书里的警告照录:
生产代码别依赖——改个函数名、过一遍压缩器,逻辑就断。12
对比两种「给实例方法」的写法,内存账一目了然:
// 写法一:构造器里挂——每个实例各拷一份函数
function Ninja() { this.skulk = function () { return this; }; }
// 写法二:prototype 上挂——所有实例共享一份
Ninja.prototype.swingSword = () => true;一个类一万个实例,写法一是一万份函数字码,写法二是两份(构造器 + 原型方法)。 「方法放原型」是 JavaScript 对象模型的内存根基——class 语法默认就是这么做的。13
13.3 动态性的三笔账与 monkey patching 的兴衰
原型的动态性(书里列了三条):原型属性随时可加可改,一改全体后备对象生效; 对象自己定义同名属性就遮蔽原型;运行中整条链可换(Object.setPrototypeOf,慎用—— 引擎对「创建时就定好链」的对象有优化)。
这套动态性早年是被重度使用的:MooTools 当年直接改 Element.prototype
给浏览器 DOM 加新功能——所谓 monkey patching(猴子补丁,运行时改别人的代码)。
今天它被普遍皱眉:class 与 TypeScript 代表的静态化路线让代码更可预测。
但书里留了一个正当的残留场景:单元测试——比如把 Date.prototype.getTimezoneOffset
改成恒定值,让时区相关的测试在任何机器上都稳定(副作用是字符串格式化不受它影响,
作者照实承认「sometimes that's the best you can do」)。14
14. 深读五:class 的糖纸之下,与私有字段的三铁律
14.1 「糖」的逐条等价证明
书里的三个 assert 把「class 是糖」钉死了,我们把等价关系再展开成表:15
class Ninja {
name: string; // TypeScript 属性声明
constructor(name: string) { this.name = name; }
swingSword() { return true; } // 简写方法
static compare(a: Ninja, b: Ninja) { return a.level - b.level; }
}| class 写法 | 等价的老写法 | 证据 |
|---|---|---|
class Ninja {...} | 一个函数(构造器)+ Ninja.prototype 对象 | ninja instanceof Ninja 成立 |
constructor(...) | 函数体本身 | Ninja 本身能被 new |
| 简写方法 | Ninja.prototype.swingSword = function... | typeof Ninja.prototype.swingSword === "function" |
| constructor 里 this.x = ... | 实例自己的属性 | Object.hasOwn(ninja, "name") |
static compare | Ninja.compare = function... | 挂在类上,不进原型 |
一句语法点书里特意标了:constructor 是关键字,不是函数名—— 换个标识符就会被当作普通实例方法。类不定义 constructor 也合法: 空构造器造空对象,或继承父类的构造器。16
14.2 extends/super 的完整规则集
书里的规则合起来六条:
extends声明链接:子类的原型链上接父类,于是ninja instanceof Person和ninja instanceof Ninja同时成立;- 子类构造器里用 this 之前必须先 super()——父类要先完成初始化,同一个 this 才存在;
- super 在构造器里是「父类构造器」;在类体别处是「父类本身」(可访问父类的 静态成员和其他方法);
- 不定义构造器 = 默认继承父类的;
- 静态属性不继承——要同样的静态成员,子类自己再写;
- 继承是单线的:一个类只能 extends 一个类,没有多继承。17
14.3 私有字段:为什么叫「field」不叫「property」
ES2022 的 # 语法是 class 话题里唯一的新机制。书里的用词讲究值得复述:
# 开头的标识符不是普通属性,所以术语用「field(字段)」——
它们有三条超出属性描述符能力范围的性质:
- 只能点号访问:
ninja.#greet行;ninja["#greet"]是 undefined—— 字符串键和私有键是两个世界,方括号语法探测不到它; - 只能类体内访问:在那对花括号之外写
#greet,是语法错误—— 不是运行时检查,是解析期就拒绝,连跑的机会都没有; - 不能被代理、枚举、删除:Proxy 的陷阱看不见私有键;Object.keys 永远列不出; delete 不允许。18
实用价值两层(书里分得清楚):硬隔离是真的访问不到,只有少数场景真正需要;
更日常的是人机工学——# 开头一眼可见是内部实现,IDE 也不会在外面
自动补全它们。给 getter/setter 对存内部值,是书里的典型用例。19
15. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 属性的三种进阶需求 | Chapter 9: Objects, prototypes, and classes | text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:36(搜「prevent skillLevel」) |
| getter/setter 两种含义 | 同上 | :43(搜「a bit loosely」) |
| Listing 9.1 名册与三行日志 | 同上 | :65(搜「Listing 9.1」) · :99(搜「Getting firstNinja: Yoshi」) |
| get/set 语法图与行业意义 | 同上 | :111(搜「The syntax for defining getters」) · :116(搜「powerhouse features」) |
| Object.keys 对照实验 | 同上 | :170(搜「two different results」) |
| 不可枚举的排除清单 | 同上 | :186(搜「excluded from」) |
| toString 不可枚举的先例 | 同上 | :192(搜「inherited from the Object prototype」) |
| 数据可枚举/方法不可枚举模式 | 同上 | :214(搜「common pattern」) |
| 只读属性(TypeError) | 同上 | :235(搜「accessor descriptors」) · :246(搜「randomNumber = 3」) |
| 密码单向属性示例 | 同上 | :252(搜「passwordHash」) |
| configurable 语义 | 同上 | :275(搜「configurable option」) |
| 数据描述符与 writable | 同上 | :297(搜「DATA DESCRIPTORS」) · :319(搜「writable」) |
| Proxy 定义与陷阱 | 同上 | :331(搜「surrogate」) · :381(搜「traps」) |
| 天皇代理人完整例子 | 同上 | :342(搜「Listing 9.3」) |
| Reflect 的定位 | 同上 | :386(搜「sibling API」) |
| 调试/mock/negative-array 用例 | 同上 | :415(搜「debugging and performance tuning」) · :420(搜「automocking」) |
| Proxy 性能警告 | 同上 | :439(搜「significant amount of extra work」) |
| 原型回退定义 | 同上 | :455(搜「Fallbacks as inheritance」) |
| yoshi/hattori/kuma 主线 | 同上 | :463(搜「Listing 9.4」) |
| 链式查找路径(Figure 9.5) | 同上 | :519(搜「stops when there are no more prototypes」) |
| setPrototypeOf 生产禁令 | 同上 | :531(搜「should be avoided」) |
| hasOwnProperty / hasOwn | 同上 | :536(搜「IS IT THE OBJECT'S OWN PROPERTY」) |
| 构造函数与 prototype 配对 | 同上 | :556(搜「inherits the constructor function's prototype」) |
| Listing 9.5 swingSword | 同上 | :569(搜「Listing 9.5」) |
| 实例方法一份共享的机制 | 同上 | :592(搜「automatically inherited」) |
| instanceof 语义与 constructor.name | 同上 | :721(搜「prototype is in the given object's prototype chain」) · :727(搜「ninja.constructor.name」) |
| 「class 做的同样的事」 | 同上 | :601(搜「unnecessarily convoluted」) · :603(搜「just a function with a prototype object」) |
| Listing 9.6 简单类 | 同上 | :671(搜「Listing 9.6」) |
| static 与清单 | 同上 | :736(搜「static keyword」) · :739(搜「Listing 9.7」) |
| extends/super 与继承链 | 同上 | :771(搜「Listing 9.8」) · :816(搜「Figure 9.7」) |
| super 纪律与单继承 | 同上 | :824(搜「must call super before it uses this」) · :830(搜「linear in JavaScript」) |
| # 私有字段三铁律 | 同上 | :835(搜「private element syntax」) · :866(搜「unique qualities」) |
| 私有字段实用价值 | 同上 | :875(搜「internal value for a getter/setter pair」) |
| 全章总结 | 同上 | :888(搜「Summary」) |
Footnotes
-
出处:「Chapter 9: Objects, prototypes, and classes」第 26–41 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:36,搜「prevent skillLevel from being modified」)。三种进阶需求的原书清单。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 43–63 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:44,搜「used a bit loosely」)。「by convention」与「language feature」的区分。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 65–104 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:65,搜「Listing 9.1」)。名册 getter/setter 完整代码;三行日志见第 99–103 段。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 121–126 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:121,搜「get and set keywords can only be used」)。get/set 仅限新建字面量或类;defineProperty 补漏。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 116–120 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:116,搜「powerhouse features」)。对 getX/setX 样板代码的行业意义判词。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 170–184 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:173,搜「Object.keys」)。两个 Object.keys 结果的对照实验。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 184–195 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:186,搜「excluded from」)。non-enumerable 的五项排除清单。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 192–195 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:193,搜「inherited from the Object prototype」);与第 214–216 段(搜「common pattern」)。toString 先例与数据/方法枚举性模式。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 229–296 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:229,搜「ACCESSOR DESCRIPTORS」)。访问器描述符家族与四开关。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 235–272 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:257,搜「Object.defineProperty(obj, "password", {」)。单向密码属性示例与 40 字符哈希断言。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 275–295 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:277,搜「configurable option」)。configurable 默认 false 的「永久」语义与 delete 报错。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 297–329 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:297,搜「DATA DESCRIPTORS」)。数据描述符家族与 writable 默认 false。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 323–325 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:323,搜「effectively permanent」)。「non-configurable + non-writable = 永久」与「可改值不可改性」的中间态。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 331–340 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:332,搜「surrogate」)。Proxy 定义与「generalization of getters and setters」的定位。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 390–395 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:390,搜「When you perform any operation on the proxy」)。陷阱分派机制与 Figure 9.2 的岔路。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 342–383 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:342,搜「Listing 9.3」)。天皇代理人的完整代码与逐项说明。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 377–383 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:378,搜「B A getter that will be invoked when」)。target/handler/trap 三术语的定义段。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 415–437 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:415,搜「debugging and performance tuning」)。三个实用坐标:调试计时、automocking、negative-array。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 439–440 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:439,搜「significant amount of extra work」)。性能警告原文。 ↩ ↩2 -
出处:「Chapter 9: Objects, prototypes, and classes」第 455–460 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:457,搜「fall back on that object's prototype」)。原型回退语义的定义与「prototype chaining」术语。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 463–530 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:463,搜「Listing 9.4」)。yoshi/hattori/kuma 三步设置与断言链。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 611–623 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:611,搜「AN OBJECT WITH NO PROTOTYPE」)。Object.prototype 尽头、({}).toString 的来历、Object.create(null) 与「Map 通常更好」的预告。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 625–654 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:638,搜「monkey patching」)。动态性三条与 MooTools 史、测试改 Date.prototype 的正当残留用例。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 556–600 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:566,搜「inherits the constructor function's prototype」)。「new X 造出的对象以 X.prototype 为原型」的关键事实与「Every function…receives an empty object」。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 721–725 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:721,搜「The instanceof keyword works by checking if the」)。instanceof 准确语义;typeof 的局限同段。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 727–733 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:727,搜「ninja.constructor.name」)。constructor.name 调试技巧及「not recommended for production code」的警告。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 601–604 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:603,搜「just a function with a prototype object」)。「class is doing the same thing」的原文判词。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 671–719 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:671,搜「Listing 9.6」)。简单类的四个断言与「constructor is a keyword, not a function name」的语法点。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 734–769 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:736,搜「static keyword」)。static 成员的定位与「Static properties are not inherited」(第 828–829 段)。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 771–823 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:775,搜「Listing 9.8」)。extends/super 示例与「a ninja is also a person」的双 instanceof。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 824–831 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:827,搜「must call super before it uses this」)。super 纪律;「linear」「no notion of multiple inheritance」见第 830–831 段。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 832–839 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:836,搜「ES2022 introduced a syntax for making properties only」)。ES2022 私有元素语法及其 class-only 性。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 866–874 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:866,搜「unique qualities」)。三铁律清单原文。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 867–868 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:867,搜「term 'field' is preferred」)。为什么叫 field 不叫 property。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 875–882 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:877,搜「developer ergonomics」)。IDE 不补全的内部字段——人机工学层价值。 ↩ -
出处:「Chapter 9: Objects, prototypes, and classes」第 84–91 段(
text/12-ch09-chapter-9-objects-prototypes-and-classes.txt:80,搜「firstNinja === "Yoshi"」);与:569(搜「Listing 9.5」);与:842(搜「Listing 9.9」)。可复现断言的代表处。 ↩