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 | 用于拦截对象的读取属性操作 |
| any |
set | 用于拦截设置属性值的操作 |
| boolean 表示是否设置成功 |
has | 用于拦截 in 操作符 |
| boolean |
deleteProperty | 用于拦截 delete 操作符 |
| boolean 表示是否删除成功 |
apply | 用于拦截函数的调用 |
| any |
construct | 用于拦截 new 操作符 |
| object |
getPrototypeOf | 用于拦截读取对象原型的操作 |
| object〡null |
setPrototypeOf | 用于拦截设置对象原型的操作 |
| boolean 表示操作是否成功 |
defineProperty | 用于拦截对象的 defineProperty() 操作 |
| boolean 表示操作是否成功 |
getOwnPropertyDescriptor | 用于拦截对象的 getOwnPropertyDescriptor() 操作 |
| object〡undefined |
ownKeys | 用于拦截 Reflect.ownKeys() |
| 一个可枚举对象 |
isExtensible | 用于拦截对象的 isExtensible() 操作 |
| boolean |
preventExtensions | 用于拦截对象的 preventExtensions() 操作 |
| 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
,可以这么处理:
- ES5
- ES6 Proxy
console.log(o.from || '') // ''
let o = {
name: 'Tim',
age: 20
}
// 代理处理器
let handler = {
get(obj, key) {
return Reflect.has(obj, key) ? obj[key] : ''
}
}
let p = new Proxy(o, handler)
console.log(p.from) // ''
五、只读化
拿走备份,不影响原始数据。
- ES5
- ES6 Proxy
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
let o = {
name: 'Tim',
price: 200
}
let d = new Proxy(o, {
get(target, key) {
return target[key]
},
set(target, key, value) {
return false
}
})
d.price = 300
console.log(d.price, d.name) // 200 "Tim"
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" ""
去掉耦合,将验证函数抽离成一个验证函数。
let o = {
name: 'Tim',
price: 200
}
let validator = (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
}
}
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" ""
七、监控上报
将捕获的错误上报。
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
实现方法:
- 每次生成一个 id
- 不可修改
- 每个实例的 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
这种方式不可以修改,但是每此都生成了一个新的,不符合要求:
class Component {
get id() {
return 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)
}
// nqwlamib
// l9ojsjiq
// gad3vm2a
// i1jew3bd
// owquntob
// rcpce268
// va6mry5v
// lvqxv0m4
// a900358x
// jahi7079
for (let i = 0; i < 10; i++) {
console.log(com2.id)
}
// vukusf5k
// rg8hyzf3
// 50vxv0hk
// tjeyes1v
// 4g8zwsxz
// 5r1cbx1k
// v9k2v7hd
// 0mgn3heb
// n0zc9v66
// rdjevl2i
com.id = 'abc'
console.log(com.id, com2.id) // 9rjmwrd9 kxdxtywe
这种方式满足要求:
class Component {
constructor() {
this.proxy = new Proxy({
id: Math.random().toString(36).slice(-8)
}, {})
}
get id() {
return this.proxy.id
}
}
let com = new Component()
let com2 = new Component()
for (let i = 0; i < 10; i++) {
console.log(com.id) // (10)e9e8jsks
}
for (let i = 0; i < 10; i++) {
console.log(com2.id) // (10)tfs2rrvg
}
com.id = 'abc'
console.log(com.id, com2.id) // e9e8jsks tfs2rrvg
九、撤销代理
除了常规代理,还可以创建临时代理,临时代理可以撤销。
一旦 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 是专门为对象设置代理器的,可以轻松监视到对象的读写过程。
相比较 defineProperty
,Proxy 的功能更强大,使用起来也更为方便,它们的区别如下:
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 | 写⼊某个属性 |
has | in 操作符 |
deleteProperty | delete 操作符 |
getProperty | Object.getPropertypeOf() |
setProperty | Object.setPrototypeOf() |
isExtensible | Object.isExtensible() |
preventExtensions | Object.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() |
ownKeys | Object.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。