Proxy(代理)是 ES6 引入的元编程特性,可以拦截并自定义对象的基本操作。
基本概念#
Proxy 可以理解为在目标对象前设置一层”拦截”,外部对该对象的访问必须先通过这层拦截:
const target = { name: '张三', age: 25,}
const handler = { get(target, property) { console.log(`读取属性: ${property}`) return target[property] },}
const proxy = new Proxy(target, handler)
proxy.name // 打印 '读取属性: name',返回 '张三'创建 Proxy#
// new Proxy(target, handler)// target: 要代理的目标对象// handler: 定义拦截行为的对象
const target = { x: 1, y: 2 }const handler = {}
// 空 handler 等于直接转发到目标对象const proxy = new Proxy(target, handler)proxy.x // 1proxy.x = 10target.x // 10(修改了原对象)get 拦截#
拦截属性读取:
const handler = { get(target, property, receiver) { // target: 目标对象 // property: 属性名 // receiver: 代理对象本身 if (property in target) { return target[property] } throw new ReferenceError(`属性 "${property}" 不存在`) },}
const proxy = new Proxy({ name: '张三' }, handler)proxy.name // '张三'// proxy.age; // ReferenceError: 属性 "age" 不存在实现链式访问#
const chain = new Proxy( {}, { get(target, property) { return chain // 返回自身,实现链式 }, })
chain.a.b.c.d // 不会报错实现数组负索引#
function createArray(...elements) { const handler = { get(target, property, receiver) { const index = Number(property) if (index < 0) { property = String(target.length + index) } return Reflect.get(target, property, receiver) }, } return new Proxy(elements, handler)}
const arr = createArray('a', 'b', 'c')arr[-1] // 'c'arr[-2] // 'b'set 拦截#
拦截属性赋值:
const handler = { set(target, property, value, receiver) { // 验证 if (property === 'age' && typeof value !== 'number') { throw new TypeError('age 必须是数字') } if (property === 'age' && (value < 0 || value > 150)) { throw new RangeError('age 必须在 0-150 之间') }
target[property] = value return true // 必须返回 true 表示成功 },}
const person = new Proxy({}, handler)person.age = 25 // OK// person.age = '25'; // TypeError// person.age = -1; // RangeError数据绑定#
function observe(obj, callback) { return new Proxy(obj, { set(target, property, value) { const oldValue = target[property] target[property] = value callback(property, value, oldValue) return true }, })}
const data = observe({ count: 0 }, (prop, newVal, oldVal) => { console.log(`${prop}: ${oldVal} -> ${newVal}`)})
data.count = 1 // 'count: 0 -> 1'data.count = 2 // 'count: 1 -> 2'has 拦截#
拦截 in 操作符:
const handler = { has(target, property) { if (property.startsWith('_')) { return false // 隐藏私有属性 } return property in target },}
const obj = new Proxy({ _private: 'secret', public: 'visible' }, handler)'_private' in obj // false'public' in obj // truedeleteProperty 拦截#
拦截 delete 操作:
const handler = { deleteProperty(target, property) { if (property.startsWith('_')) { throw new Error('不能删除私有属性') } delete target[property] return true },}
const obj = new Proxy({ _private: 1, public: 2 }, handler)delete obj.public // OK// delete obj._private; // ErrorownKeys 拦截#
拦截 Object.keys()、Object.getOwnPropertyNames() 等:
const handler = { ownKeys(target) { // 过滤掉以下划线开头的属性 return Object.keys(target).filter((key) => !key.startsWith('_')) },}
const obj = new Proxy({ name: '张三', _password: '123456', age: 25 }, handler)
Object.keys(obj) // ['name', 'age']getOwnPropertyDescriptor 拦截#
const handler = { getOwnPropertyDescriptor(target, property) { if (property.startsWith('_')) { return undefined // 隐藏私有属性 } return Object.getOwnPropertyDescriptor(target, property) },}
const obj = new Proxy({ _secret: 1, public: 2 }, handler)Object.getOwnPropertyDescriptor(obj, '_secret') // undefinedObject.getOwnPropertyDescriptor(obj, 'public') // { value: 2, ... }defineProperty 拦截#
拦截 Object.defineProperty():
const handler = { defineProperty(target, property, descriptor) { if (property.startsWith('_')) { return false // 禁止定义私有属性 } return Object.defineProperty(target, property, descriptor) },}
const obj = new Proxy({}, handler)Object.defineProperty(obj, 'name', { value: '张三' }) // OK// Object.defineProperty(obj, '_secret', { value: 'x' }); // 静默失败或抛错可撤销代理#
Proxy.revocable() 创建可撤销的代理:
const { proxy, revoke } = Proxy.revocable({ name: '张三' }, {})
proxy.name // '张三'
revoke() // 撤销代理
// proxy.name; // TypeError: Cannot perform 'get' on a proxy that has been revoked实际应用:
// 临时授权访问function createTemporaryAccess(target, timeout) { const { proxy, revoke } = Proxy.revocable(target, {})
setTimeout(revoke, timeout)
return proxy}
const sensitiveData = { secret: '机密信息' }const tempAccess = createTemporaryAccess(sensitiveData, 5000)
tempAccess.secret // '机密信息'// 5秒后自动撤销访问权限this 指向问题#
Proxy 会改变 this 指向:
const target = { name: '张三', sayName() { console.log(this.name) },}
const proxy = new Proxy(target, {})
target.sayName() // '张三'(this 指向 target)proxy.sayName() // '张三'(this 指向 proxy)
// 大多数情况下没问题,但某些内置对象需要注意某些内置对象需要绑定 this#
const target = new Date()const proxy = new Proxy(target, {})
// proxy.getDate(); // TypeError
// 需要绑定 thisconst handler = { get(target, property) { if (typeof target[property] === 'function') { return target[property].bind(target) } return target[property] },}
const dateProxy = new Proxy(new Date(), handler)dateProxy.getDate() // OK实战:表单验证#
function createValidatedObject(validators) { return new Proxy( {}, { set(target, property, value) { const validator = validators[property] if (validator && !validator.validate(value)) { throw new Error(validator.message) } target[property] = value return true }, } )}
const user = createValidatedObject({ name: { validate: (v) => typeof v === 'string' && v.length >= 2, message: 'name 必须是至少2个字符的字符串', }, age: { validate: (v) => Number.isInteger(v) && v >= 0 && v <= 150, message: 'age 必须是 0-150 的整数', }, email: { validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'email 格式不正确', },})
user.name = '张三' // OKuser.age = 25 // OK// user.age = -1; // Error: age 必须是 0-150 的整数小结#
| 拦截操作 | 触发条件 |
|---|---|
| get | 读取属性 |
| set | 设置属性 |
| has | in 操作符 |
| deleteProperty | delete 操作符 |
| ownKeys | Object.keys() 等 |
| getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
| defineProperty | Object.defineProperty() |
Proxy 是 JavaScript 元编程的核心工具,可以实现数据验证、数据绑定、访问控制等功能。