Skip to main content

Proxy 实现代理

一、Proxy 的定义

Proxy 用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),被用于许多库和浏览器框架上,例如 Vue3 的数据响应式。

// 需要包装的对象,可以是任何变量
const target = {}
// 代理配置,通常是用捕捉器函数作为属性值的对象
const handler = {
get(target, key, recevier) {
return target[key]
},
set(target, key, val, recevier) {
target[key] = val
return true
}
}
const proxy = new Proxy(target, handler)

二、Proxy 的捕捉器函数

可选的捕捉器函数如下:

捕捉器函数劫持函数参数函数返回值
get用于拦截对象的读取属性操作
  1. target: 目标对象
  2. property: 被获取的属性名
  3. receiver: Proxy 或继承 Proxy 的对象
any
set用于拦截设置属性值的操作
  1. target: 目标对象
  2. property: 被设置的属性名
  3. value: 新属性值
  4. receiver: Proxy 或继承 Proxy 的对象
boolean 表示是否设置成功
has用于拦截 in 操作符
  1. target: 目标对象
  2. property: 被检查的属性名
boolean
deleteProperty用于拦截 delete 操作符
  1. target: 目标对象
  2. property: 被删除的属性名
boolean 表示是否删除成功
apply用于拦截函数的调用
  1. target: 目标对象
  2. thisArg: 被调用时的上下文对象
  3. argumentsList: 被调用时的参数数组
any
construct用于拦截 new 操作符
  1. target: 目标对象
  2. argumentsList: constructor 的参数列表
  3. newTarget: 最初被调用的构造函数
object
getPrototypeOf用于拦截读取对象原型的操作
  1. target: 目标对象
object〡null
setPrototypeOf用于拦截设置对象原型的操作
  1. target: 目标对象
  2. prototype: 对象新原型或为 null
boolean 表示操作是否成功
defineProperty用于拦截对象的 defineProperty() 操作
  1. target: 目标对象
  2. property: 待检索其描述的属性名
  3. descriptor: 待定义或修改的属性的描述符
boolean 表示操作是否成功
getOwnPropertyDescriptor用于拦截对象的 getOwnPropertyDescriptor() 操作
  1. target: 目标对象
  2. property: 返回属性名称的描述
object〡undefined
ownKeys用于拦截 Reflect.ownKeys()
  1. target: 目标对象
一个可枚举对象
isExtensible用于拦截对象的 isExtensible() 操作
  1. target: 目标对象
boolean
preventExtensions用于拦截对象的 preventExtensions() 操作
  1. target: 目标对象
boolean 表示操作是否成功

三、基本用法

let p = new Proxy(target, handler)

参数:

  • target(必须):用来代理的 “对象”,被代理之后它是不能直接被访问的。
  • handler(必须):实现代理的过程。

示例:

// 设 o 为房东,即被代理的对象
let o = {
name: 'Tim',
price: 200
}

// 设 d 为中介,即代理
let d = new Proxy(o, {})

console.log(d.price, d.name) // 200 'Tim'

// 因为传的是空对象,所以是透传

let d = new Proxy(o, {
get(target, key) { // target 是指被代理的对象 o,key 指的是 o 的属性
// 如果 key 为价格时进行加 10 的操作,否则返回 key 的值本身
if (key === 'price') {
return target[key] + 10
} else {
return target[key]
}
}
})

console.log(d.price, d.name) // 210 "Tim"

上述对代理后的 d 中 price 进行相应处理(中介报价在房东报价之上)

四、读取不存在的属性

再举个例子,当我们要读取一个对象中不存在的属性时,由于对象没有这个属性,所以会返回 undefined

let o = {
name: 'Tim',
age: 20
}

console.log(o.name) // Tim
console.log(o.age) // 20
console.log(o.from) // undefined

如果我们不想在调用的时候出现 undefined,可以这么处理:

console.log(o.from || '') // ''

五、只读化

拿走备份,不影响原始数据。

let o = {
name: 'Tim',
price: 200
}

for (let [key] of Object.entries(o)) {
Object.defineProperty(o, key, {
writable: false
})
}

console.log(o.name, o.price) // Tim 200

o.price = 300
console.log(o.name, o.price) // Tim 200

ES5 做法和 ES6 代理的区别在于 ES5 的全部锁死,而 ES6 中用户只读,但是代理可以做操作。

六、校验处理

实现:如果价格 >300 就不让修改,没有这个属性则返回空字符串。

let o = {
name: 'Tim',
price: 200
}

let d = new Proxy(o, {
get(target, key) {
return target[key] || ''
},
set(target, key, value) {
if (Reflect.has(target, key)) {
if (key === 'price') {
if (value > 300) {
return false
} else {
target[key] = value
}
} else {
target[key] = value
}
} else {
return false
}
}
})

d.price = 240
console.log(d.price, d.name) // 240 "Tim"

d.price = 301 // 没有生效,因为校验没有通过
d.name = 'Bang'
console.log(d.price, d.name) // 240 "Bang"

d.age = 23 // 没有这个属性,set 时候返回,get 的时候赋值为空字符串
console.log(d.price, d.name, d.age) // 240 "Bang" ""

七、监控上报

将捕获的错误上报。

window.addEventListener('error', (e) => {
console.log(e.message)
// 上报
// report('...')
}, true) // 捕获

let o = {
name: 'Tim',
price: 200
}

let validator = (target, key, value) => {
if (Reflect.has(target, key)) {
if (key === 'price') {
if (value > 300) {
// 不满足要触发错误
throw new TypeError('price exceed 300')
} else {
target[key] = value
}
} else {
target[key] = value
}
} else {
return false
}
}

let d = new Proxy(o, {
get(target, key) {
return target[key] || ''
},
set: validator
})

d.price = 240
console.log(d.price, d.name) // 240 "Tim"

d.price = 301
d.name = 'Bang'
console.log(d.price, d.name) // 240 "Bang"

d.age = 23
console.log(d.price, d.name, d.age) // 240 "Bang" ""

八、唯一只读 id

实现方法:

  1. 每次生成一个 id
  2. 不可修改
  3. 每个实例的 id 互不相同

这种方式可以每次生成一个 id,但是可以修改,不符合要求:

class Component {
constructor() {
this.id = Math.random().toString(36).slice(-8)
}
}

let com = new Component()
let com2 = new Component()

for (let i = 0; i < 10; i++) {
console.log(com.id) // (10) 030nc7is
}
for (let i = 0; i < 10; i++) {
console.log(com2.id) // (10) 772fqaup
}

com.id = 'abc'
console.log(com.id, com2.id) // abc 93ukz26i

九、撤销代理

除了常规代理,还可以创建临时代理,临时代理可以撤销。

一旦 revoke 被调用,Proxy 就失效了,就起到了临时代理的作用。

let o = {
name: 'Tim',
price: 200
}

// 这里不能使用 new,只能使用 Proxy.revocable 去声明代理
let d = Proxy.revocable(o, {
get(target, key) {
if (key === 'price') {
return target[key] + 10
} else {
return target[key]
}
}
})
// d 里面包含了代理数据和撤销操作
console.log(d.proxy.price) // 210
console.log(d) // {proxy: Proxy, revoke: ƒ}

setTimeout(function () {
// 对代理进行撤销操作
d.revoke()
setTimeout(function () {
console.log(d.proxy.price)
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
}, 100)
}, 1000)

十、Proxy VS Object.defineProperty()

如果想监听对象属性的改变,可以使用 Object.defineProperty 这个方法去添加属性,捕捉对象中属性的读写过程,Vue2 就是通过这个实现的数据双向绑定,而 Vue3 开始则使用 Proxy 来实现双向绑定。

Proxy 是专门为对象设置代理器的,可以轻松监视到对象的读写过程。

相比较 definePropertyProxy 的功能更强大,使用起来也更为方便,它们的区别如下:

1、Proxy 可以监听的类型更多

defineProperty 只能监视属性的读写,Proxy 能够监视到更多对象的操作,例如删除属性操作。

const person = {
name: 'Tim',
age: 23
}

const personProxy = new Proxy(person, {
deleteProperty(target, property) {
console.log('delete ' + property) // delete age
delete target[property]
}
})

delete personProxy.age

console.log(person) // { name: 'Tim' }
handler ⽅法触发⽅式
get读取某个属性
set写⼊某个属性
hasin 操作符
deletePropertydelete 操作符
getPropertyObject.getPropertypeOf()
setPropertyObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty()
ownKeysObject.keys() 、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()
apply调⽤⼀个函数
construct⽤ new 调⽤⼀个函数

另外,Object.defineProperty() 使用的是重写数组的操作方法,Proxy 拥有更好的数组对象监视。

Proxy 对数组的监视如下:

const list = []

const listProxy = new Proxy(list, {
set(target, property, value) {
console.log('set', property, value)
target[property] = value
return true // 表示设置成功
}
})

listProxy.push(100)
// set 0 100
// set length 1

listProxy.push(200)
// set 1 200
// set length 2

2、Proxy 以非入侵的方式监听对象读写

definedProperty 劫持的是对象属性,新增元素需再次 definedProperty;而 Proxy 劫持的是整个对象,不需要做特殊处理。

3、defineProperty 的兼容性更好

Proxy 不兼容IE,也没有 polyfill, 而 defineProperty 能支持到 IE9。