Skip to content

Proxy 基础

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 // 1
proxy.x = 10
target.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 // true

deleteProperty 拦截#

拦截 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; // Error

ownKeys 拦截#

拦截 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') // undefined
Object.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
// 需要绑定 this
const 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 = '张三' // OK
user.age = 25 // OK
// user.age = -1; // Error: age 必须是 0-150 的整数

小结#

拦截操作触发条件
get读取属性
set设置属性
hasin 操作符
deletePropertydelete 操作符
ownKeysObject.keys() 等
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty()

Proxy 是 JavaScript 元编程的核心工具,可以实现数据验证、数据绑定、访问控制等功能。