JavaScript Proxy 元编程:实现响应式、验证器和 ORM 的底层利器
前言
Proxy 是 ES6 引入的元编程(Metaprogramming)原语,它允许你拦截和自定义对象的基本操作——属性读取、赋值、函数调用、in 运算符、delete 操作等等。如果说普通编程是”写操作数据的代码”,那么元编程就是”写操作代码的代码”。
Vue 3 的响应式系统、各种数据验证库、ORM 框架的查询构建器,底层都大量使用了 Proxy。本文将全面解析 Proxy 的 handler traps、Reflect API,并通过三个实战项目(响应式系统、数据验证框架、迷你 ORM)展示 Proxy 的强大威力。
Proxy 基础
创建 Proxy
const target = { name: 'Alice', age: 30 };
const handler = { get(target, property, receiver) { console.log(`Reading ${String(property)}`); return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { console.log(`Writing ${String(property)} = ${value}`); return Reflect.set(target, property, value, receiver); }};
const proxy = new Proxy(target, handler);
proxy.name; // Reading name → "Alice"proxy.age = 31; // Writing age = 31Proxy 接收两个参数:
- target:被代理的原始对象
- handler:一个对象,定义了各种拦截操作的”陷阱”(traps)
所有 Handler Traps 一览
Proxy 支持 13 个陷阱,覆盖了对象的所有基本操作:
const fullHandler = { // 属性读取: proxy.foo, proxy['foo'] get(target, property, receiver) {},
// 属性赋值: proxy.foo = bar set(target, property, value, receiver) {},
// 属性存在检查: 'foo' in proxy has(target, property) {},
// 属性删除: delete proxy.foo deleteProperty(target, property) {},
// Object.keys(), for...in 等 ownKeys(target) {},
// Object.getOwnPropertyDescriptor(proxy, prop) getOwnPropertyDescriptor(target, property) {},
// Object.defineProperty(proxy, prop, descriptor) defineProperty(target, property, descriptor) {},
// Object.getPrototypeOf(proxy) getPrototypeOf(target) {},
// Object.setPrototypeOf(proxy, proto) setPrototypeOf(target, prototype) {},
// Object.isExtensible(proxy) isExtensible(target) {},
// Object.preventExtensions(proxy) preventExtensions(target) {},
// proxy(...args) — 仅当 target 是函数时 apply(target, thisArg, argumentsList) {},
// new proxy(...args) — 仅当 target 是构造函数时 construct(target, argumentsList, newTarget) {},};Reflect API:Proxy 的最佳拍档
Reflect 对象为每个 Proxy trap 提供了对应的默认操作方法。使用 Reflect 而不是直接操作 target 的原因:
- 保持正确的 receiver:确保原型链上的 getter/setter 正确工作
- 返回值语义一致:
Reflect.set()返回 boolean,与settrap 的期望返回值一致 - 避免异常:某些操作如
Object.defineProperty失败会抛异常,Reflect.defineProperty返回 false
// 错误示范:直接操作 targetconst badHandler = { get(target, prop) { return target[prop]; // 不传 receiver,原型链上的 getter 可能出错 }, set(target, prop, value) { target[prop] = value; // 不返回 boolean return true; }};
// 正确示范:使用 Reflectconst goodHandler = { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); }, set(target, prop, value, receiver) { return Reflect.set(target, prop, value, receiver); }};Receiver 的重要性
const parent = { get greeting() { return `Hello, I'm ${this.name}`; }};
const child = Object.create( new Proxy(parent, { get(target, prop, receiver) { console.log('receiver is child?', receiver === child); return Reflect.get(target, prop, receiver); // receiver 确保 this 指向 child } }));
child.name = 'Bob';console.log(child.greeting);// receiver is child? true// "Hello, I'm Bob"如果不传 receiver,this.name 将在 parent 上查找,得到 undefined。
实战一:Vue 3 风格响应式系统
Vue 3 的响应式核心就是基于 Proxy 实现的。下面我们从零实现一个简化版:
依赖收集与触发
// 当前正在执行的副作用函数let activeEffect = null;const effectStack = [];
// 存储依赖关系: target -> property -> Set<effect>const targetMap = new WeakMap();
function track(target, property) { if (!activeEffect) return;
let depsMap = targetMap.get(target); if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); }
let deps = depsMap.get(property); if (!deps) { deps = new Set(); depsMap.set(property, deps); }
deps.add(activeEffect);}
function trigger(target, property) { const depsMap = targetMap.get(target); if (!depsMap) return;
const deps = depsMap.get(property); if (!deps) return;
// 创建副本避免无限循环 const effectsToRun = new Set(deps); effectsToRun.forEach(effect => { // 避免递归触发自身 if (effect !== activeEffect) { effect(); } });}reactive() 实现
const reactiveMap = new WeakMap();
function reactive(target) { // 避免重复代理 if (reactiveMap.has(target)) { return reactiveMap.get(target); }
const proxy = new Proxy(target, { get(target, property, receiver) { // 特殊标记,用于判断是否是 reactive 对象 if (property === '__isReactive') return true; if (property === '__raw') return target;
track(target, property);
const result = Reflect.get(target, property, receiver);
// 深层代理:如果值是对象,递归创建 reactive if (result !== null && typeof result === 'object') { return reactive(result); }
return result; },
set(target, property, value, receiver) { const oldValue = target[property]; const result = Reflect.set(target, property, value, receiver);
// 只在值真正变化时触发 if (oldValue !== value && (oldValue === oldValue || value === value)) { trigger(target, property); }
return result; },
has(target, property) { track(target, property); return Reflect.has(target, property); },
deleteProperty(target, property) { const hadKey = Object.prototype.hasOwnProperty.call(target, property); const result = Reflect.deleteProperty(target, property);
if (hadKey && result) { trigger(target, property); }
return result; },
ownKeys(target) { // 当 for...in 或 Object.keys() 时追踪 track(target, Symbol('iterate')); return Reflect.ownKeys(target); } });
reactiveMap.set(target, proxy); return proxy;}effect() 实现
function effect(fn) { const effectFn = () => { // 清除旧的依赖关系 cleanup(effectFn);
activeEffect = effectFn; effectStack.push(effectFn);
const result = fn();
effectStack.pop(); activeEffect = effectStack[effectStack.length - 1];
return result; };
effectFn.deps = []; effectFn();
return effectFn;}
function cleanup(effectFn) { for (const deps of effectFn.deps) { deps.delete(effectFn); } effectFn.deps.length = 0;}computed() 实现
function computed(getter) { let value; let dirty = true;
const effectFn = effect(() => { value = getter(); dirty = false; });
// 重写 effect 的自动执行行为 // 这里简化实现,实际 Vue 使用 scheduler return { get value() { if (dirty) { value = getter(); dirty = false; } return value; } };}完整使用示例
const state = reactive({ firstName: 'John', lastName: 'Doe', todos: [ { text: 'Learn Proxy', done: false }, { text: 'Build reactive system', done: false } ]});
// 自动追踪依赖effect(() => { console.log(`Full name: ${state.firstName} ${state.lastName}`);});// 立即输出: "Full name: John Doe"
state.firstName = 'Jane';// 自动输出: "Full name: Jane Doe"
state.lastName = 'Smith';// 自动输出: "Full name: Jane Smith"
// 深层响应式effect(() => { const pending = state.todos.filter(t => !t.done).length; console.log(`Pending todos: ${pending}`);});// 立即输出: "Pending todos: 2"
state.todos[0].done = true;// 自动输出: "Pending todos: 1"实战二:数据验证框架
利用 Proxy 的 set trap,可以实现强大的运行时数据验证:
Schema 定义
const Types = { string: { validate: v => typeof v === 'string', message: 'must be a string' }, number: { validate: v => typeof v === 'number' && !isNaN(v), message: 'must be a number' }, boolean: { validate: v => typeof v === 'boolean', message: 'must be a boolean' }, email: { validate: v => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'must be a valid email' }, url: { validate: v => { try { new URL(v); return true; } catch { return false; } }, message: 'must be a valid URL' }, array: (itemValidator) => ({ validate: v => Array.isArray(v) && v.every(item => itemValidator.validate(item)), message: `must be an array of valid items` }), oneOf: (...values) => ({ validate: v => values.includes(v), message: `must be one of: ${values.join(', ')}` }), range: (min, max) => ({ validate: v => typeof v === 'number' && v >= min && v <= max, message: `must be between ${min} and ${max}` })};验证 Proxy 工厂
class ValidationError extends Error { constructor(property, value, message) { super(`Validation failed for "${property}": ${message} (got ${JSON.stringify(value)})`); this.property = property; this.value = value; }}
function createValidatedModel(schema, options = {}) { const { strict = true, onError = 'throw', coerce = false } = options; const errors = [];
const handler = { set(target, property, value, receiver) { const rule = schema[property];
// 严格模式:不允许设置 schema 中未定义的属性 if (strict && !rule && typeof property === 'string') { const err = new ValidationError(property, value, 'property not defined in schema'); if (onError === 'throw') throw err; errors.push(err); return false; }
if (rule) { // 类型强制转换 if (coerce && rule.coerce) { value = rule.coerce(value); }
// 执行所有验证器 const validators = Array.isArray(rule) ? rule : [rule]; for (const validator of validators) { if (!validator.validate(value)) { const err = new ValidationError(property, value, validator.message); if (onError === 'throw') throw err; errors.push(err); return false; } } }
return Reflect.set(target, property, value, receiver); },
get(target, property, receiver) { if (property === '__errors') return [...errors]; if (property === '__schema') return schema; if (property === '__isValid') return errors.length === 0; return Reflect.get(target, property, receiver); },
deleteProperty(target, property) { if (strict && schema[property]?.required) { throw new ValidationError(property, undefined, 'required property cannot be deleted'); } return Reflect.deleteProperty(target, property); } };
// 创建初始对象并设置默认值 const initial = {}; for (const [key, rule] of Object.entries(schema)) { if (rule.default !== undefined) { initial[key] = rule.default; } }
return new Proxy(initial, handler);}使用示例
const user = createValidatedModel({ name: { validate: v => typeof v === 'string' && v.length >= 2, message: 'must be a string with at least 2 characters', required: true }, age: Types.range(0, 150), email: Types.email, role: Types.oneOf('admin', 'user', 'moderator'), tags: Types.array(Types.string)});
// 正常赋值user.name = 'Alice';user.age = 30;user.email = 'alice@example.com';user.role = 'admin';user.tags = ['developer', 'blogger'];
console.log(user.name); // "Alice"console.log(user.age); // 30
// 验证失败try { user.age = 200; // ValidationError: must be between 0 and 150} catch (e) { console.error(e.message);}
try { user.email = 'not-an-email'; // ValidationError: must be a valid email} catch (e) { console.error(e.message);}
try { user.role = 'superadmin'; // ValidationError: must be one of: admin, user, moderator} catch (e) { console.error(e.message);}嵌套验证
function nestedModel(schema) { return { validate: v => typeof v === 'object' && v !== null, message: 'must be an object', transform: v => createValidatedModel(schema).assign(v) };}
// 深度代理版本function deepValidated(schema) { const handler = { set(target, property, value, receiver) { const rule = schema[property];
if (rule && typeof value === 'object' && value !== null && rule.schema) { // 嵌套对象也进行验证代理 value = deepValidated(rule.schema); Object.assign(value, value); }
if (rule && !rule.validate(value)) { throw new ValidationError(property, value, rule.message); }
return Reflect.set(target, property, value, receiver); },
get(target, property, receiver) { return Reflect.get(target, property, receiver); } };
return new Proxy({}, handler);}实战三:迷你 ORM 查询构建器
利用 Proxy 的 get trap 可以实现优雅的链式 API:
class QueryBuilder { #table; #conditions = []; #selections = ['*']; #ordering = []; #limitVal = null; #offsetVal = null; #joins = [];
constructor(table) { this.#table = table; }
select(...fields) { this.#selections = fields; return this; }
where(condition, ...params) { this.#conditions.push({ clause: condition, params }); return this; }
orderBy(field, direction = 'ASC') { this.#ordering.push(`${field} ${direction}`); return this; }
limit(n) { this.#limitVal = n; return this; }
offset(n) { this.#offsetVal = n; return this; }
join(table, on) { this.#joins.push(`JOIN ${table} ON ${on}`); return this; }
leftJoin(table, on) { this.#joins.push(`LEFT JOIN ${table} ON ${on}`); return this; }
toSQL() { let sql = `SELECT ${this.#selections.join(', ')} FROM ${this.#table}`;
if (this.#joins.length) { sql += ' ' + this.#joins.join(' '); }
if (this.#conditions.length) { const where = this.#conditions.map(c => c.clause).join(' AND '); sql += ` WHERE ${where}`; }
if (this.#ordering.length) { sql += ` ORDER BY ${this.#ordering.join(', ')}`; }
if (this.#limitVal !== null) { sql += ` LIMIT ${this.#limitVal}`; }
if (this.#offsetVal !== null) { sql += ` OFFSET ${this.#offsetVal}`; }
return sql; }
getParams() { return this.#conditions.flatMap(c => c.params); }}用 Proxy 实现魔术方法
function createModel(tableName, columns) { const model = { tableName, columns,
query() { return new QueryBuilder(tableName); },
find(id) { return new QueryBuilder(tableName) .where('id = ?', id) .limit(1); },
all() { return new QueryBuilder(tableName); } };
// 使用 Proxy 实现动态 findByXxx 方法 return new Proxy(model, { get(target, property, receiver) { if (property in target) { return Reflect.get(target, property, receiver); }
const propStr = String(property);
// findByXxx 动态方法 if (propStr.startsWith('findBy')) { const field = propStr.slice(6) // 去掉 'findBy' .replace(/([A-Z])/g, '_$1') // camelCase → snake_case .toLowerCase() .replace(/^_/, '');
if (columns.includes(field)) { return function (value) { return new QueryBuilder(tableName) .where(`${field} = ?`, value); }; } }
// whereXxx 动态条件 if (propStr.startsWith('where')) { const field = propStr.slice(5) .replace(/([A-Z])/g, '_$1') .toLowerCase() .replace(/^_/, '');
return function (operator, value) { if (value === undefined) { value = operator; operator = '='; } return new QueryBuilder(tableName) .where(`${field} ${operator} ?`, value); }; }
return undefined; } });}
// 使用const User = createModel('users', ['id', 'name', 'email', 'age', 'created_at']);
console.log(User.find(1).toSQL());// SELECT * FROM users WHERE id = ? LIMIT 1
console.log(User.findByEmail('alice@example.com').toSQL());// SELECT * FROM users WHERE email = ?
console.log(User.findByName('Alice').orderBy('created_at', 'DESC').toSQL());// SELECT * FROM users WHERE name = ? ORDER BY created_at DESC
console.log( User.query() .select('name', 'email') .where('age > ?', 18) .where('name LIKE ?', '%alice%') .orderBy('name') .limit(10) .offset(20) .toSQL());// SELECT name, email FROM users WHERE age > ? AND name LIKE ? ORDER BY name LIMIT 10 OFFSET 20用 Proxy 实现属性变更追踪(Dirty Checking)
function trackable(obj) { const original = { ...obj }; const changes = new Map();
const proxy = new Proxy(obj, { set(target, property, value, receiver) { const oldValue = target[property];
if (oldValue !== value) { if (original[property] === value) { changes.delete(property); } else { changes.set(property, { old: oldValue, new: value }); } }
return Reflect.set(target, property, value, receiver); },
get(target, property, receiver) { if (property === '$isDirty') return changes.size > 0; if (property === '$changes') return Object.fromEntries(changes); if (property === '$reset') { return () => { for (const [key, val] of Object.entries(original)) { target[key] = val; } changes.clear(); }; } if (property === '$commit') { return () => { Object.assign(original, target); changes.clear(); }; }
return Reflect.get(target, property, receiver); } });
return proxy;}
// 使用const record = trackable({ name: 'Alice', age: 30, email: 'alice@test.com' });
record.name = 'Bob';record.age = 25;
console.log(record.$isDirty); // trueconsole.log(record.$changes);// { name: { old: 'Alice', new: 'Bob' }, age: { old: 30, new: 25 } }
record.name = 'Alice'; // 恢复原值console.log(record.$changes);// { age: { old: 30, new: 25 } } — name 的变更自动清除
record.$reset();console.log(record.name); // "Alice"console.log(record.age); // 30console.log(record.$isDirty); // false高级技巧
可撤销代理(Revocable Proxy)
function createSecureSession(data, ttl = 5000) { const { proxy, revoke } = Proxy.revocable(data, { get(target, property, receiver) { console.log(`[SESSION] Reading ${String(property)}`); return Reflect.get(target, property, receiver); } });
// TTL 过期后自动撤销 setTimeout(() => { revoke(); console.log('[SESSION] Expired and revoked'); }, ttl);
return { session: proxy, revoke };}
const { session } = createSecureSession({ token: 'abc123', user: 'admin' }, 3000);
console.log(session.token); // [SESSION] Reading token → "abc123"
// 3 秒后...// [SESSION] Expired and revoked// session.token → TypeError: Cannot perform 'get' on a proxy that has been revoked负索引数组
function negativeArray(arr) { return new Proxy(arr, { get(target, property, receiver) { const index = Number(property); if (Number.isInteger(index) && index < 0) { property = String(target.length + index); } return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { const index = Number(property); if (Number.isInteger(index) && index < 0) { property = String(target.length + index); } return Reflect.set(target, property, value, receiver); } });}
const arr = negativeArray([1, 2, 3, 4, 5]);console.log(arr[-1]); // 5console.log(arr[-2]); // 4arr[-1] = 99;console.log(arr); // [1, 2, 3, 4, 99]不可变对象(Immutable Object)
function immutable(obj) { return new Proxy(obj, { set() { throw new TypeError('Cannot modify immutable object'); }, deleteProperty() { throw new TypeError('Cannot delete from immutable object'); }, defineProperty() { throw new TypeError('Cannot define property on immutable object'); }, setPrototypeOf() { throw new TypeError('Cannot change prototype of immutable object'); }, get(target, property, receiver) { const value = Reflect.get(target, property, receiver); // 深层不可变 if (value !== null && typeof value === 'object') { return immutable(value); } return value; } });}
const config = immutable({ db: { host: 'localhost', port: 5432 }, cache: { ttl: 3600 }});
console.log(config.db.host); // "localhost"
try { config.db.host = 'remote'; // TypeError: Cannot modify immutable object} catch (e) { console.error(e.message);}性能考量
Proxy 虽然强大,但有性能成本:
- 每次属性访问都经过 trap:在热路径上,Proxy 比直接属性访问慢 5-10 倍
- 无法被 V8 内联优化:引擎无法对 Proxy 对象进行常规的 hidden class 优化
- 深层代理的递归开销:每次读取嵌套对象都会创建新的 Proxy(Vue 3 通过缓存优化了这个问题)
优化建议:
// ❌ 不要在性能关键路径上使用 Proxyfor (let i = 0; i < 1000000; i++) { sum += proxyObj.value; // 每次都经过 get trap}
// ✅ 先解引用,再循环const value = proxyObj.value;for (let i = 0; i < 1000000; i++) { sum += value;}
// ✅ 缓存深层代理const reactiveCache = new WeakMap();function reactive(obj) { if (reactiveCache.has(obj)) return reactiveCache.get(obj); const proxy = new Proxy(obj, handler); reactiveCache.set(obj, proxy); return proxy;}Proxy 的不变量(Invariants)
JavaScript 引擎会强制执行某些不变量,防止 Proxy 违反语言基本约定:
const obj = {};Object.defineProperty(obj, 'x', { value: 42, writable: false, configurable: false});
const proxy = new Proxy(obj, { get(target, property) { return 100; // 试图返回与实际不同的值 }});
// TypeError: 'get' on proxy: property 'x' is a read-only and non-configurable// data property on the proxy target but the proxy did not return its actual valueconsole.log(proxy.x);这些不变量确保了即使使用 Proxy,JavaScript 的核心语义也不会被破坏。
总结
Proxy 是 JavaScript 元编程的终极工具:
- 响应式系统:拦截读写操作,实现自动依赖收集和更新触发
- 数据验证:在赋值时自动校验,保证数据完整性
- ORM/查询构建:通过
gettrap 实现动态方法和魔术属性 - 变更追踪:自动记录对象修改历史
- 访问控制:实现只读、可撤销、有时效的对象访问
配合 Reflect API,你可以在保持语言语义正确的前提下,对对象的行为进行任意定制。理解 Proxy 的 13 个 trap 和它们的不变量约束,是成为 JavaScript 高级开发者的必备技能。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!