开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。
红宝书《JavaScript高级程序设计(第5版)》学习大纲 - 大前端全栈开发 - SegmentFault 思否
Proxy 与 Reflect 的分工
Proxy 和 Reflect 是 ES6 同期引入的一对互补 API:Proxy 负责”拦截”,Reflect 负责”在拦截后正确地执行默认操作”。
但并非所有场景都需要 Reflect。以下按对象属性的三种形态,逐步说明 target[key] 与 Reflect.get(target, key, receiver) 的差异。
前置概念:属性存取器与 Proxy 陷阱的同名异质
get 与 set 以两种不同面目出现在 JavaScript 中,但二者并非名称的巧合:handler 中的 get/set 陷阱与属性存取器的 [[Get]]/[[Set]],底层拦截的正是同一条规范内部方法调用链。区别在于挂载的层级不同——一个挂在属性描述符层,一个挂在对象代理层。明确这两个层次的差异,对后续讨论 Proxy 与 Reflect 的协作至关重要。
第一种面目是属性描述符中的存取器(accessor descriptor)。当通过 Object.defineProperty 或对象字面量中的 get key() {} 语法为某个属性配置 getter/setter 时,属性本身的性质发生了根本改变:该属性不再拥有 [[Value]] 槽位,取而代之的是 [[Get]] 与 [[Set]] 两个函数引用。读取该属性时,引擎不再从 [[Value]] 中取值,而是调用 [[Get]] 并返回其返回值。属性键名依旧存在,但”键值对”这一概念在此处已不适用,因为值不是存储的,而是每次读取时由函数临时计算产出的。此机制作用于单个属性级别,一个对象的不同属性可以各自独立地选择数据描述符或存取描述符。
第二种面目是 Proxy 构造函数的 handler 对象中的 get/set 陷阱(trap)。handler 中的 get(target, key, receiver) 并非某个属性的描述符,而是一道拦截整个对象所有属性读写操作的关卡。其核心性质可概括为三点:其一,拦截发生在对象层面,而非属性层面,所有属性(无论新增还是已有)的读写一律经过这同一道陷阱;其二,陷阱本身不改变目标对象的属性描述符,目标对象上使用数据描述符的属性依然保有 [[Value]],使用存取描述符的属性依然保有 [[Get]]/[[Set]],陷阱仅在读写操作到达目标对象之前插入一段自定义逻辑;其三,陷阱的返回值是最终暴露给外部的值,但源数据仍然完好地存储在目标对象的对应槽位中。
这两种机制的核心差异在于”值”的归属。存取描述符将值的产出逻辑直接写在属性自身,属性不再持有 [[Value]];Proxy 陷阱则将拦截逻辑写在对象的外部包装层上,目标对象的 [[Value]] 以及其他描述符字段全程未被触碰。因此,同一个属性在被 Proxy 包装后,通过 proxy 访问会经过陷阱并触发依赖收集,通过原始对象直接访问则不受任何影响,依然返回原始的 [[Value]]。存取描述符不具备这种”隔离性”,getter 一旦被定义,无论从哪个引用读取该属性,走的是同一条 [[Get]] 函数路径。
从 ECMAScript 规范的角度看,这两种机制对应的内部方法调用路径不同。存取描述符的 getter 在
[[Get]](P, receiver)内部方法的第 5 步被调用,属于”属性解析完成后发现描述符为存取型”这一分支。Proxy 的get陷阱则在[[Get]]内部方法更早的阶段触发:当[[Get]]发现目标对象是 Proxy 实例时,跳转至[[GetP]]内部方法,进而调用 handler 中注册的get函数。同样的名称(get),挂载的位置和时机完全不同。
情况一:纯数据属性::target[key] 可以直接工作
假设原始对象仅包含普通属性(data descriptor,无 getter/setter):
1
2
3
4
5
6
7
const obj = { name: 'hello', age: 18 };
const proxy = new Proxy(obj, {
get(target, key) {
return target[key]; // 正常工作
}
});
proxy.name; // "hello"
普通的属性访问不涉及函数调用,不存在 this 绑定对象的问题。target[key] 直接取到属性的值,两种写法在此场景下行为完全一致。
情况二:原始对象自身存在 getter/setter::target[key] 失效
getter 和 setter 并非 Proxy 的专属机制。ES5 的 Object.defineProperty() 即可将属性的描述符从数据描述符改为存取描述符,使属性的读写行为由一个函数定义(参见本书 read10 篇):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = {};
Object.defineProperty(obj, 'nickname', {
get() {
return this._nickname; // 读取 this._nickname 时触发什么?取决于 this 指向谁
}
});
const proxy = new Proxy(obj, {
get(target, key) {
return target[key]; // getter 调用时 this = obj(原始对象),不是 proxy
}
});
proxy.nickname;
// 内部:obj.nickname 的 getter 被调用
// getter 中的 this 指向 obj
// this._nickname → obj._nickname :: 绕过了 proxy 的 get 陷阱
target[key] 的本质是 target.[[Get]](key, target)::[[Get]] 内部方法收到的 receiver 是 target(原始对象)。当引擎沿属性描述符找到 getter 时,以 receiver(即原始对象)为 this 调用该 getter。proxy 在此流程中被完全跳过。
情况三:getter/setter 在原型链上::同样失效
若 getter 定义在原型链上层对象上,问题相同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const parent = {
get nickname() {
return this._nickname; // this 指向谁,谁就需要过代理陷阱
}
};
const child = Object.create(parent);
const proxy = new Proxy(child, {
get(target, key) {
return target[key]; // 引擎沿原型链找到 parent.nickname 的 getter
}
});
proxy.nickname;
// target[key] → target.[[Get]](key, target)
// 原型链查找 → 在 parent 上找到 nickname 的 getter
// getter 的 this = target = child(原始对象),非 proxy
// this._nickname 的读取不经过 proxy 的 get 陷阱
根因一致:target[key] 将 [[Get]] 的 receiver 固定为原始对象,无论 getter 定义在自身还是原型链上,this 均绑定为原始对象。
Reflect 的修正
Reflect.get(target, key, receiver) 将 proxy 作为 receiver 传入 [[Get]]:
1
2
3
4
5
6
7
8
9
10
const proxy = new Proxy(child, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver); // receiver = proxy
}
});
proxy.nickname;
// Reflect.get(target, key, receiver) → target.[[Get]](key, receiver=proxy)
// getter 的 this = receiver = proxy
// getter 内部 this._nickname → proxy._nickname → 再次触发 proxy 的 get 陷阱 ✅
路线差异可以精确总结为:
| 写法 | [[Get]] 的 receiver | getter 的 this | getter 内部的属性访问是否过 proxy |
|---|---|---|---|
target[key] | 原始对象 | 原始对象 | ❌ |
Reflect.get(target, key, receiver) | proxy | proxy | ✅ |
结论:何时需要 Reflect
Reflect.get/set 的必要性仅限于一种情形::原始对象或其原型链上存在 getter/setter。对于仅含普通属性的对象,target[key] 行为正确。
那么为何 Vue 3 的响应式系统在任何场景都走 Reflect.get?因为框架无法预先知道使用者会不会在某处定义了 getter、或者某个对象的原型链上是否潜伏了一个 getter。逐属性检查”是否有 getter”会引入性能开销和代码分支复杂度。走 Reflect.get 是一条统一切换线::它不挑场景,对任何属性访问都保持行为统一,是一种”无需判断”的设计选择。
Proxy 的 13 个陷阱方法与 Reflect 的 13 个方法一一对应,属于规范层面的刻意设计:每个 Proxy 陷阱的默认行为恰好是其对应的 Reflect 方法。Reflect 本身不执行任何显式的 this 修正动作::它仅仅请求引擎”按规范完成一次属性读取/写入”,receiver 是这次请求中附带的一个参数。receiver 在属性为普通 data descriptor 时不参与行为,仅在遇到 getter/setter 时被引擎用于
this绑定。
属性访问与函数调用触发两次陷阱
Proxy 的 get 陷阱在属性访问时触发,但在方法调用场景中存在一个值得注意的现象:proxy.method() 会触发两次 get 陷阱,而非一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = {
name: '小明',
getName() {
return this.name;
}
};
const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log('读取:', key);
return Reflect.get(target, key, receiver);
}
});
proxy.getName();
// 读取: getName
// 读取: name
两次触发的原因在于 JavaScript 引擎对 proxy.getName() 的处理天然分为两个阶段:
第一阶段:proxy.getName,属性访问。引擎先获取 getName 属性的值,触发 get 陷阱,key = "getName"。return Reflect.get(target, key, receiver) 返回 obj.getName 所指向的函数对象。
第二阶段:(),函数调用。引擎执行获取到的函数。函数体内部的 return this.name 是一次独立的属性访问:this 指向 proxy,访问 this.name 再次触发 get 陷阱,key = "name"。
两次陷阱并非 Proxy 的特殊机制,而是普通 JavaScript 语义在代理层上的投影:属性访问和函数调用是两个独立步骤,代理忠实地为每一步执行拦截。
若不用 Reflect 而直接
return target[key],内部this.name的this将指向谁?这正是下一节讨论的 Reflect 核心价值所在。
不用 Reflect 时的 this 绑定问题
一个常见的误解是”不用 Reflect 时 this 就一定指向原始对象”。实际上,对于普通的 proxy.method() 调用而言,不用 Reflect 也不会产生错误:因为 proxy.method() 的执行上下文天然是 proxy,JavaScript 引擎按调用点规则将 this 正确绑定。真正暴露问题的是继承链和回调传递两种场景。
场景一:普通方法不受影响,getter 才会暴露问题
对于普通的原型链方法调用,target[key] 没有问题::因为 child.getName() 的调用点语法让 JS 引擎将 this 绑给 child(即 proxy),跟函数定义在原型链上还是自身无关:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const parent = {
name: '父级',
getName() {
return this.name;
}
};
const child = new Proxy(Object.create(parent), {
get(target, key) {
console.log('读取:', key);
return target[key]; // 不使用 Reflect
}
});
child.getName();
// 读取: getName
// 读取: name
// 两次 get 陷阱正常触发::this 就是 proxy,没有问题
真正暴露 receiver 缺失的场景是原型链上的 getter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const parent = {
get nickname() {
return this._nickname || '匿名';
}
};
const child = new Proxy(Object.create(parent), {
get(target, key) {
return target[key]; // ❌ 缺失 receiver
}
});
child.nickname;
// getter 触发时 this = parent(不是 proxy)
// 若 getter 内进一步访问 this.xxx,不会过代理陷阱
修正方式相同::传入 receiver:
1
2
3
4
5
6
7
const child2 = new Proxy(Object.create(parent), {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
});
// 读取: nickname
// getter 内 this 被 receiver(proxy)修正,再次访问 this._nickname 也会过 proxy 陷阱
Reflect.get 的第三个参数 receiver 即为此问题而设。receiver 在 Proxy 的 get 陷阱中始终是 proxy 自身。Reflect.get(target, key, receiver) 向 JavaScript 引擎指明:访问 getter 时,this 应指向调用链最前端的 receiver,而非属性真实所在的那个原型链上层对象。
场景二:方法被当作回调传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
name: '小明',
getName() {
return this.name;
}
};
const proxy = new Proxy(obj, {
get(target, key) {
return target[key]; // 不使用 Reflect
}
});
const fn = proxy.getName; // 只触发一次 get 陷阱
fn(); // this === undefined(严格模式),this.name 报错
此处的问题根源不在 Proxy 也不在 Reflect:fn() 是裸函数调用,任何函数以这种方式调用,其 this 均为 undefined(严格模式下)。但这揭示了一个更深层的事实:Proxy 的 get 陷阱中 return target[key] 返回的是原始函数对象,该函数丢失了”最初从 proxy 上调出”的调用上下文。使用 .bind(proxy) 或在 get 陷阱中返回已绑定 this 的函数可以弥补,但真正系统性的解决方案是始终使用 Reflect.get 保持 receiver 链的完整性。
在实际框架代码中(如 Vue 3 的响应式系统),Reflect 的使用是标准做法:
Reflect.get(target, key, receiver)确保所有属性访问(包括方法内部的this.xxx)全部经过代理管道,依赖收集不产生遗漏。
Reflect 的设计目的与核心价值
Reflect 的设计目的
Reflect 是 ES6 与 Proxy 同时引入的内置对象,其方法集合与 Proxy 的陷阱方法一一对应:
| Proxy 陷阱 | Reflect 方法 | 操作 |
|---|---|---|
get | Reflect.get() | 读取属性 |
set | Reflect.set() | 设置属性 |
has | Reflect.has() | in 操作符 |
deleteProperty | Reflect.deleteProperty() | delete 操作符 |
getPrototypeOf | Reflect.getPrototypeOf() | 获取原型 |
setPrototypeOf | Reflect.setPrototypeOf() | 设置原型 |
apply | Reflect.apply() | 函数调用 |
construct | Reflect.construct() | new 操作符 |
| 等共 13 个 | 等共 13 个 |
该设计的逻辑在于:每一个 Proxy 陷阱的默认行为恰好是对应的 Reflect 方法。因此 Proxy 中写 return Reflect.get(target, key, receiver) 的语义是”除自定义逻辑外,其余行为与原对象一致”。若直接使用 target[key],将丢失 receiver 这一关键参数,以及 getter 中 this 绑定等若干边界行为。
Reflect 的三个核心价值(一个核心,两个附属)
核心:纠正 this 指向。如前两节所述:Reflect.get(target, key, receiver) 的第三个参数确保属性访问的 this 指向 receiver(proxy),而非原始对象或原型链上级的对象。与 Proxy 搭配使用时,这一能力是无可替代的::除 Reflect.get 和 Reflect.set 外,没有任何其他机制能将 receiver 递入 ECMAScript 规范的 [[Get]] 或 [[Set]] 内部方法。
其运作机制可精确描述如下。当执行 proxy.nickname 时,引擎调用 proxy.[[Get]]('nickname', proxy),进入 Proxy 的 get 陷阱,参数 receiver 为 proxy 自身。若陷阱中写 return target[key],等价于 target.[[Get]]('nickname', target)::引擎沿原型链查找到 nickname 对应的 getter,以 target(即 parent)为 this 调用该 getter。而 return Reflect.get(target, key, receiver) 等价于 target.[[Get]]('nickname', receiver)::引擎以 receiver(即 proxy)为 this 调用 getter,this 由此被导向 proxy。
关键事实:Reflect.get 并未执行任何显式的 this 修正动作。它仅仅将 receiver 原样传递给 [[Get]] 内部方法,而引擎在遇到 accessor descriptor(getter/setter)时使用 receiver 作为 getter.call(receiver) 的 this。对于普通 data descriptor(无 getter/setter 的属性),receiver 在 [[Get]] 内部流程中无任何用途::这正是前一节中普通方法调用不受影响的根本原因。
Reflect.get/set的 receiver 传递是核心能力。另外两个附属价值并非与 Proxy 绑定::Reflect的其他方法在不使用 Proxy 的场景同样可用。但 receiver 这唯一的活,没有Reflect.get/set就无法完成。
附属一:返回布尔值代替抛异常。若干传统操作符在失败时抛出异常或静默失败,Reflect 对应方法统一返回布尔值或结果值:
1
2
3
4
5
6
7
// 传统写法:失败抛异常
Object.defineProperty(obj, 'name', { value: 'test' }); // 可能抛 TypeError
// Reflect 写法:失败返回 false,不中断执行流
Reflect.defineProperty(obj, 'name', { value: 'test' }); // 返回 true 或 false
Reflect.set(obj, 'name', 'value'); // 返回布尔值
Reflect.deleteProperty(obj, 'name'); // 返回布尔值
此设计使 Reflect 在框架代码中免于大量 try-catch,且与 Proxy 对应陷阱的布尔返回值约定天然对齐::陷阱中直接 return Reflect.xxx(...) 即可贯通默认行为与自定义逻辑。
附属二:将元操作统一为函数调用。部分语言操作符在 ES5 时代缺少对应的函数形式,Reflect 完成了这一补充:
| 操作符 | Reflect 函数 |
|---|---|
delete obj.key | Reflect.deleteProperty(obj, key) |
key in obj | Reflect.has(obj, key) |
new F(...) | Reflect.construct(F, args) |
obj.key | Reflect.get(obj, key) |
func.apply(thisArg, args) | Reflect.apply(func, thisArg, args) |
这使元操作可被作为一等公民传递,例如 ['name', 'age'].map(Reflect.get.bind(null, obj))。同时 Reflect.apply 和 Reflect.construct 作为 Reflect 上的静态方法,不会被对象自有属性覆盖,对比 Function.prototype.apply 可能被同名实例属性遮蔽,具有更高的操作安全性。
Reflect 与 Proxy 在 Vue 3 中的协同
Vue 3 响应式系统中,reactive() 使用 Proxy 创建响应式对象时,每一个陷阱方法均配套了一个 Reflect 调用。这不是可选的优化,而是功能性前提。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Vue 3 reactive 的极简骨架
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 依赖收集
return Reflect.get(target, key, receiver); // 默认读取行为 + receiver 绑定
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 派发更新
return result; // 返回操作是否成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
}
});
}
Reflect 在此处的角色是让 Proxy 的默认行为正确发生的必要条件。若移除 Reflect,Vue 的响应式依赖收集在嵌套对象和继承场景下会产生遗漏:其后果不是”不够好”,而是直接失效。
Proxy可理解为”修改行为的钩子”,Reflect则为”执行默认行为的标准路径”。Proxy 负责在读写时附加自定义逻辑,Reflect 负责按规范执行默认操作并保持this指向正确。两者从 ES6 设计之初即为互补:Reflect 的 13 个方法与 Proxy 的 13 个陷阱一一对应,属于规范层面的刻意设计。