系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染
- 第 18 篇:DOM 交互补充
- 第 19 篇:DOM 实战案例
- 第 20 篇:CSS 布局与可视化高频
- 第 21 篇:移动端与 viewport
- 第 22 篇:BOM 核心对象
- 第 23 篇:前端路由原理
- 第 24 篇:浏览器存储对比
- 第 25 篇:网络与跨域
- 第 26 篇:网络请求与实时通道
- 第 27 篇:Service Worker、PWA 与 Web Worker
- 第 28 篇:浏览器高级 API
- 第 29 篇:图片懒加载
- 第 30 篇:ES6+ 模块
- 第 31 篇:Symbol 与 Iterator / Generator
- 第 32 篇:Proxy 与 Reflect(本文)
文章目录
- 系列文章目录
- 前言
- [一、Proxy 是什么](#一、Proxy 是什么)
- [二、13 种 Trap 全览](#二、13 种 Trap 全览)
-
- [2.1 属性读写类(最常用)](#2.1 属性读写类(最常用))
-
- [`get(target, prop, receiver)`](#
get(target, prop, receiver)) - [`set(target, prop, value, receiver)`](#
set(target, prop, value, receiver)) - [`has(target, prop)`](#
has(target, prop))
- [`get(target, prop, receiver)`](#
- [2.2 属性描述类](#2.2 属性描述类)
-
- [`defineProperty(target, prop, descriptor)`](#
defineProperty(target, prop, descriptor)) - [`getOwnPropertyDescriptor(target, prop)`](#
getOwnPropertyDescriptor(target, prop)) - [`deleteProperty(target, prop)`](#
deleteProperty(target, prop))
- [`defineProperty(target, prop, descriptor)`](#
- [2.3 遍历与原型类](#2.3 遍历与原型类)
-
- `ownKeys(target)`
- `getPrototypeOf(target)`
- [`setPrototypeOf(target, proto)`](#
setPrototypeOf(target, proto))
- [2.4 函数调用类](#2.4 函数调用类)
-
- [`apply(target, thisArg, argumentsList)`](#
apply(target, thisArg, argumentsList)) - [`construct(target, argumentsList, newTarget)`](#
construct(target, argumentsList, newTarget))
- [`apply(target, thisArg, argumentsList)`](#
- [2.5 其他](#2.5 其他)
-
- [`isExtensible(target)` / `preventExtensions(target)`](#
isExtensible(target)/preventExtensions(target))
- [`isExtensible(target)` / `preventExtensions(target)`](#
- [三、Reflect:Proxy 的"另一半"](#三、Reflect:Proxy 的"另一半")
- 四、Proxy.revocable:可撤销的代理
- [五、Vue 3 为什么选择 Proxy](#五、Vue 3 为什么选择 Proxy)
-
- [5.1 Object.defineProperty 的局限(Vue 2)](#5.1 Object.defineProperty 的局限(Vue 2))
- [5.2 Proxy 的优势(Vue 3)](#5.2 Proxy 的优势(Vue 3))
- 六、实际应用场景
-
- [6.1 数据验证](#6.1 数据验证)
- [6.2 日志与调试](#6.2 日志与调试)
- [6.3 负索引数组](#6.3 负索引数组)
- [6.4 不可变对象(深度冻结)](#6.4 不可变对象(深度冻结))
- 七、易混淆点
- 八、思考与练习
- 总结
前言
上一篇讲了 Symbol 是元编程的入口之一,本篇继续元编程主题:Proxy 与 Reflect。
如果说 Symbol.iterator 是让对象"告诉"语言如何遍历自己,那 Proxy 就是让对象把所有基本操作都暴露给开发者拦截。读属性、写属性、删除、遍历、函数调用......几乎每一个你对对象能做的事情,Proxy 都能插入自定义逻辑。
而 Reflect,很多人觉得它只是"把 Object 上的方法搬了一遍",但它的真正价值在于和 Proxy 的 trap 一一对应 ,解决 receiver 传递问题。
本篇会讲清楚:Proxy 的 13 种 trap 分别拦截什么?Reflect 存在的意义是什么?为什么 Vue 3 从 Object.defineProperty 切换到了 Proxy?
一、Proxy 是什么
Proxy 可以理解为一个拦截层,包裹在目标对象外面,所有对目标对象的操作都会先经过 Proxy:
外部操作 → Proxy(trap 拦截)→ 目标对象
基本语法:
javascript
const target = { name: "Alice", age: 25 };
const handler = {
get(target, prop, receiver) {
console.log(`读取了 ${prop}`);
return target[prop];
}
};
const proxy = new Proxy(target, handler);
proxy.name; // 控制台输出:"读取了 name",返回 "Alice"
关键点:
new Proxy(target, handler)创建代理对象target是被代理的原始对象handler是一个普通对象,其属性名是 trap 名,属性值是拦截函数- 不定义某个 trap 时,操作直接穿透到 target,没有额外开销
二、13 种 Trap 全览
Proxy 支持 13 种 trap,覆盖了对象的基本操作。我按使用频率和重要性分组讲解。
2.1 属性读写类(最常用)
get(target, prop, receiver)
拦截属性读取 :proxy.foo、proxy["foo"]。
javascript
const handler = {
get(target, prop) {
if (prop === "age") {
return target[prop] < 0 ? 0 : target[prop];
}
return Reflect.get(target, prop);
}
};
const person = new Proxy({ age: -5 }, handler);
console.log(person.age); // 0(拦截修正了负数)
set(target, prop, value, receiver)
拦截属性赋值 :proxy.foo = 1。
javascript
const handler = {
set(target, prop, value) {
if (prop === "age" && (typeof value !== "number" || value < 0)) {
throw new TypeError("age 必须是非负数");
}
target[prop] = value;
return true; // 必须返回 true,否则严格模式下抛 TypeError
}
};
const person = new Proxy({}, handler);
person.age = 25; // 正常
// person.age = -1; // TypeError: age 必须是非负数
面试重点:set 必须返回 true 。在严格模式下,如果 set trap 返回 falsy 值,会抛出 TypeError。这是因为规范要求 [[Set]] 内部方法返回一个布尔值表示是否成功。
has(target, prop)
拦截 in 操作符:"foo" in proxy。
javascript
const handler = {
has(target, prop) {
if (prop.startsWith("_")) {
return false; // 下划线开头的属性"隐藏"
}
return prop in target;
}
};
const obj = new Proxy({ _secret: 42, name: "Alice" }, handler);
console.log("_secret" in obj); // false
console.log("name" in obj); // true
注意:has 只拦截 in 操作符,不影响 Object.keys() 或 for...in。
2.2 属性描述类
defineProperty(target, prop, descriptor)
拦截 Object.defineProperty() 和 proxy.prop = value(后者也会触发)。
getOwnPropertyDescriptor(target, prop)
拦截 Object.getOwnPropertyDescriptor()。
deleteProperty(target, prop)
拦截 delete proxy.prop。
javascript
const handler = {
deleteProperty(target, prop) {
if (prop === "id") {
throw new Error("id 属性不可删除");
}
delete target[prop];
return true; // 必须返回 true,否则严格模式报错
}
};
const obj = new Proxy({ id: 1, name: "test" }, handler);
delete obj.name; // 正常
// delete obj.id; // Error: id 属性不可删除
2.3 遍历与原型类
ownKeys(target)
拦截以下操作:
Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环
javascript
const handler = {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith("_"));
}
};
const obj = new Proxy({ _internal: 1, visible: 2 }, handler);
console.log(Object.keys(obj)); // ["visible"]
注意:ownKeys 必须返回一个数组,且不能包含目标对象上不存在的属性,否则会报错。
getPrototypeOf(target)
拦截 Object.getPrototypeOf(proxy) 和 proxy instanceof Constructor 的原型查找。
setPrototypeOf(target, proto)
拦截 Object.setPrototypeOf(proxy, proto)。
2.4 函数调用类
apply(target, thisArg, argumentsList)
拦截函数调用 :proxy()。只能用于函数对象。
javascript
function sum(a, b) { return a + b; }
const handler = {
apply(target, thisArg, args) {
console.log(`调用参数:${args}`);
const result = target.apply(thisArg, args);
console.log(`返回结果:${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, handler);
proxiedSum(1, 2); // "调用参数:1,2" → "返回结果:3"
construct(target, argumentsList, newTarget)
拦截 new 操作符:new proxy()。只能用于构造函数。
javascript
class User {
constructor(name) { this.name = name; }
}
const handler = {
construct(target, args, newTarget) {
console.log(`创建实例:${args}`);
return new target(...args);
}
};
const ProxiedUser = new Proxy(User, handler);
new ProxiedUser("Alice"); // "创建实例:Alice"
2.5 其他
isExtensible(target) / preventExtensions(target)
拦截 Object.isExtensible() 和 Object.preventExtensions()。
三、Reflect:Proxy 的"另一半"
3.1 Reflect 的存在意义
Reflect 是 ES6 新增的内置对象 ,它的方法和 Proxy 的 trap 一一对应。这不是巧合,而是有意设计。
Reflect 解决的核心问题是:在 Proxy trap 中,如何正确地调用默认行为?
错误示范
javascript
const handler = {
get(target, prop, receiver) {
// 直接用 target[prop] 有问题!
return target[prop];
}
};
const parent = new Proxy({ x: 1 }, handler);
const child = Object.create(parent);
child.x; // 如果 x 是 getter,this 会指向 parent 而不是 child
正确写法
javascript
const handler = {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
};
Reflect.get(target, prop, receiver) 会把 receiver 正确传递下去,确保 this 指向正确。
3.2 receiver 是什么
receiver 是 Proxy 链中的"最终调用者"。考虑这个场景:
javascript
const parent = {
get greet() {
return `Hello, I'm ${this.name}`;
}
};
const child = Object.create(parent);
child.name = "Child";
console.log(child.greet); // "Hello, I'm Child"
this 正确指向 child,因为 JavaScript 内部用 receiver 来传递"谁在调用"。
但如果用 Proxy:
javascript
const handler = {
get(target, prop, receiver) {
// 错误:return target[prop];
// 正确:
return Reflect.get(target, prop, receiver);
}
};
const proxiedParent = new Proxy(parent, handler);
const child = Object.create(proxiedParent);
child.name = "Child";
console.log(child.greet); // 正确:"Hello, I'm Child"
如果用 target[prop] 代替 Reflect.get,this 会指向 target(原始 parent),而不是 child。
3.3 Reflect 的其他用途
除了和 Proxy 配合,Reflect 还有一些实用价值:
javascript
// 1. 替代 Object 上的一些方法(返回布尔值,更规范)
Reflect.defineProperty(obj, prop, descriptor); // 返回 true/false
Reflect.deleteProperty(obj, prop); // 返回 true/false
// 2. 替代 delete 操作符(函数形式,更明确)
delete obj.prop; // 旧写法
Reflect.deleteProperty(obj, prop); // Reflect 写法
// 3. 替代 in 操作符
Reflect.has(obj, prop); // 等价于 prop in obj
// 4. 安全地调用 Object 方法(避免对象不是对象时的异常)
Reflect.apply(fn, thisArg, args); // 替代 fn.apply(thisArg, args)
一句话总结 :Reflect 是操作对象的函数式 API,语义更清晰,错误处理更规范。
四、Proxy.revocable:可撤销的代理
new Proxy() 创建的代理是不可撤销的。如果你需要在某些场景下禁用代理 ,用 Proxy.revocable:
javascript
const target = { secret: 42 };
const { proxy, revoke } = Proxy.revocable(target, {
get(target, prop) {
return target[prop];
}
});
console.log(proxy.secret); // 42
revoke(); // 撤销代理
// 之后任何操作都会报错
// proxy.secret; // TypeError: Cannot perform 'get' on a proxy that has been revoked
// proxy.foo = 1; // TypeError
应用场景:
- 一次性访问令牌:授权后立即撤销
- 安全沙箱:代码执行完毕后禁用对敏感对象的访问
- 缓存失效:当缓存过期时,撤销代理以强制重新获取
javascript
function createSecureAPI(data) {
const { proxy, revoke } = Proxy.revocable(data, {
get(target, prop) {
if (prop === "revoke") return revoke;
return Reflect.get(target, prop);
}
});
return proxy;
}
const api = createSecureAPI({ getUser: () => ({ name: "Alice" }) });
api.getUser(); // { name: "Alice" }
api.revoke(); // 撤销
// api.getUser(); // TypeError
五、Vue 3 为什么选择 Proxy
这是面试高频问题,理解 Proxy 相比 Object.defineProperty 的优势是关键。
5.1 Object.defineProperty 的局限(Vue 2)
javascript
const obj = {};
Object.defineProperty(obj, "name", {
get() { console.log("读取 name"); return this._name; },
set(val) { console.log("设置 name"); this._name = val; }
});
obj.name = "Alice"; // "设置 name"
obj.name; // "读取 name"
问题 1:无法监听新增属性
javascript
const state = { a: 1 };
// 只劫持了 a
Object.defineProperty(state, "b", { /* ... */ }); // 需要手动添加
// Vue 2 必须用 $set
this.$set(this.obj, "newProp", 123);
问题 2:无法监听数组索引和长度
javascript
const arr = [1, 2, 3];
arr[0] = 10; // 不会触发 setter
arr.length = 0; // 不会触发 setter
// Vue 2 只能重写数组方法(push, pop, splice 等)
问题 3:需要递归遍历所有属性
javascript
// Vue 2 初始化时要遍历整个 data 对象
function observe(obj) {
for (const key in obj) {
defineReactive(obj, key, obj[key]);
if (typeof obj[key] === "object") {
observe(obj[key]); // 递归
}
}
}
5.2 Proxy 的优势(Vue 3)
javascript
const state = new Proxy({ a: 1 }, {
get(target, prop, receiver) {
track(target, prop); // 依赖收集
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
trigger(target, prop); // 触发更新
return result;
}
});
state.b = 2; // 自动触发响应式
state.arr[0] = 1; // 自动触发响应式
优势总结:
| 对比项 | Object.defineProperty | Proxy |
|---|---|---|
| 新增属性 | 需要 $set | 自动监听 |
| 数组索引 | 需要重写方法 | 自动监听 |
| 删除属性 | 无法监听 | deleteProperty trap |
| 性能 | 初始化递归遍历 | 惰性代理(访问时才代理子对象) |
| 代码复杂度 | 需要重写数组方法 | 统一 trap 处理 |
惰性代理是关键优化:
javascript
const state = new Proxy({ nested: { deep: { value: 1 } } }, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === "object" && value !== null) {
// 只有访问时才代理子对象
return new Proxy(value, this);
}
return value;
}
});
// 只有访问 nested 时才会代理 nested
// 访问 nested.deep 时才会代理 deep
state.nested.deep.value; // 代理链按需创建
六、实际应用场景
6.1 数据验证
javascript
function createValidated(schema) {
return new Proxy({}, {
set(target, prop, value) {
const validate = schema[prop];
if (validate && !validate(value)) {
throw new TypeError(`${prop} 验证失败`);
}
return Reflect.set(target, prop, value);
}
});
}
const user = createValidated({
age: v => typeof v === "number" && v >= 0 && v <= 150,
email: v => typeof v === "string" && v.includes("@")
});
user.age = 25; // 正常
// user.age = -1; // TypeError
// user.email = "invalid"; // TypeError
6.2 日志与调试
javascript
function createLogger(obj, name = "Object") {
return new Proxy(obj, {
get(target, prop, receiver) {
console.log(`[${name}] get ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`[${name}] set ${String(prop)} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
deleteProperty(target, prop) {
console.log(`[${name}] delete ${String(prop)}`);
return Reflect.deleteProperty(target, prop);
}
});
}
const state = createLogger({ count: 0 }, "Counter");
state.count++; // "[Counter] get count" → "[Counter] set count = 1"
6.3 负索引数组
javascript
function createArray(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
if (typeof prop === "string" && /^-?\d+$/.test(prop)) {
const index = Number(prop);
if (index < 0) {
return Reflect.get(target, target.length + index, receiver);
}
}
return Reflect.get(target, prop, receiver);
}
});
}
const arr = createArray([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
6.4 不可变对象(深度冻结)
javascript
function deepFreeze(obj) {
return new Proxy(obj, {
set() { throw new Error("只读对象"); },
deleteProperty() { throw new Error("只读对象"); },
defineProperty() { throw new Error("只读对象"); },
setPrototypeOf() { throw new Error("只读对象"); },
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === "object" && value !== null) {
return deepFreeze(value); // 递归代理
}
return value;
}
});
}
const frozen = deepFreeze({ nested: { value: 1 } });
// frozen.nested.value = 2; // Error: 只读对象
// frozen.nested = {}; // Error: 只读对象
七、易混淆点
- Proxy 不是透明的 :
typeof proxy === "object",但proxy === target为false。Proxy 是一个新对象,不是 target 本身。 settrap 必须返回true:否则严格模式下抛 TypeError。这是规范要求,不是可选的。has不拦截Object.keys():has只拦截in操作符。遍历用ownKeys。get的receiver不总是 proxy :当通过原型链访问时,receiver是实际发起访问的对象(可能是子对象)。- Proxy 不能代理原始值 :
new Proxy(1, handler)会报错,Proxy 只能代理对象。 - Reflect 和 Object 方法的区别:Reflect 方法返回布尔值表示成功/失败,Object 方法可能抛错或返回对象本身。
- Proxy 是浅层的 :默认情况下,嵌套对象不会自动被代理,需要在
gettrap 中递归创建。
八、思考与练习
1. 以下代码输出什么?为什么?
javascript
const target = {};
const proxy = new Proxy(target, {});
proxy.a = 1;
console.log(target.a);
解析:输出 1。没有定义 trap 时,操作直接穿透到 target。proxy 和 target 共享同一个底层对象。
2. 为什么 Proxy 不能代理 Map、Set 等内置对象?
解析:Map、Set 的内部方法(如 [[MapData]])依赖 this 指向原始对象。直接代理会导致 this 指向 proxy,内部方法报错。需要特殊处理:
javascript
const map = new Map();
const proxy = new Proxy(map, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === "function") {
return value.bind(target); // 绑定到原始对象
}
return value;
}
});
3. Vue 3 的 reactive() 是如何用 Proxy 实现的?为什么需要 track 和 trigger?
解析:track 在 get 中收集依赖(记录哪些组件用到了这个属性),trigger 在 set 中触发更新(通知用到这个属性的组件重新渲染)。这是响应式系统的核心。
4. Proxy.revocable 和直接用 Proxy 有什么区别?
解析:Proxy.revocable 返回 { proxy, revoke },可以调用 revoke() 永久禁用代理。普通 Proxy 一旦创建就无法撤销。
5. 如何用 Proxy 实现一个简单的 JSON.stringify 替代品,只序列化可枚举的自有属性?
javascript
function safeStringify(obj) {
const proxy = new Proxy(obj, {
ownKeys(target) {
return Object.keys(target); // 只返回可枚举自有属性
}
});
return JSON.stringify(proxy);
}
6. 为什么 Reflect.get(target, prop, receiver) 比 target[prop] 更安全?
解析:当 prop 是 getter 时,target[prop] 的 this 指向 target;Reflect.get 会把 receiver 传递给 getter,确保 this 指向正确的对象(可能是 proxy 或子对象)。
总结
- Proxy 是对象操作的拦截层,13 种 trap 覆盖几乎所有基本操作。
- Reflect 和 Proxy trap 一一对应,解决
receiver传递问题,是操作对象的规范 API。 - Vue 3 选择 Proxy 是因为它能自动监听新增属性、数组索引、删除操作,且支持惰性代理优化性能。
- Proxy 的核心价值是元编程:让开发者能透明地拦截和自定义对象行为,而不改变原始代码。
settrap 必须返回true,gettrap 中用Reflect.get传递receiver是最佳实践。
下一篇讲 Map / Set / WeakMap / WeakSet:键类型、顺序、弱引用集合与 DOM 元数据关联(系列第 33 篇,大纲 §33)。