首先, 我们先来看看Proxy的作用
Proxy — 代理对象, 用于包装另一个对象, 拦截其读取属性、写入属性或者其他操作, 相当于为对象包裹了一层拦截器

使用
构建Proxy对象的语法为
const proxy = new Proxy(target, handler)
target: 需要被代理的对象(可以是函数)handler: 被代理对象的捕获器(trap)配置
基础使用
举个例子
const target = {}
// 空代理
const proxy = new Proxy(target, {})
// 下面我们为proxy赋值
proxy.a = 1
// 因为我们没有为target设置任何代理配置, 相当于一个空代理,所以对代理对象proxy的赋值会如实转发给原对象target
console.log(target)
// -> {a: 1}
console.log(proxy)
// -> Proxy {a: 1}
上面的例子中我们创建了一个空的代理对象, 未设置任何捕获器, 所以它不会对目标对象的操作做任何处理,保留源对象默认行为.
handler可配置捕获器一览
get: 读取操作捕获器set: 赋值操作捕获器has:in操作符捕获器getPrototypeOf:[Object.getPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/GetPrototypeOf)方法的捕捉器。setPrototypeOf:[Object.setPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf)方法的捕捉器。ownkeys:[Object.getOwnPropertyNames](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames)方法和[Object.getOwnPropertySymbols](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols)方法的捕捉器defineProperty:[Object.defineProperty](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)方法的捕捉器。deleteProperty:[delete](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/delete)操作符的捕捉器。getOwnPropertyDescriptor:[Object.getOwnPropertyDescriptor](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)方法的捕捉器。isExtensible:[Object.isExtensible](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible)方法的捕捉器。preventExtensions:[Object.preventExtensions](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions)方法的捕捉器。apply: 函数调用操作的捕捉器。construct:[new](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)操作符的捕捉器。
注: 对创建的Proxy对象进行操作才会触发这些捕获器, 对源对象操作不会触发任何捕获器.
get捕获器
通过配置handler对象的get方法来为对象的**读取操作(底层的[[get]])**设置代理
get(target, prop, receiver): any
target: 被代理对象prop: 将要被读取的属性名称receiver:
下面我们来创建一个代理了其取值(get)方法的代理对象, 比如数组对象取-1下标值, 我们让他返回数组最后一个元素
const proxy = new Proxy([1,2,3,4,5], {
get (target, prop) {
// 这里 prop 接收到的属性为 string | symbol 类型
if (prop === '-1') {
return target[target.length - 1]
}
return target[prop]
}
})
console.log(proxy[-1]) // -> 5
proxy.push(6)
console.log(proxy[-1]) // -> 6
注: prop接收到属性名为字符串类型, 像数组下标这样的数字也会被转化为string (Symbol依旧是symbol)
set捕获器
通过配置handler的set方法可以为对象的**赋值操作(底层的[[set]])**设置代理
set(target, prop, value, receiver): boolean
target: 被代理对象prop: 将要被赋值的属性名称value: 被赋值属性的值receiver:
比如我们让一个数组只可以放入数字类型(number)的值
const proxy = new Proxy([], {
set(target, prop, value) {
if (typeof value === 'number') {
target[prop] = value
return true
} else {
throw TypeError('This array can only accept numbers')
}
}
})
proxy.push('1') // -> TypeError: This array can only accept numbers
在上面的例子中, 我们将 throw TypeError去掉, 即只有在value为number的时候才返回true, 其余不操作
const proxy = new Proxy([], {
set(target, prop, value) {
if (typeof value === 'number') {
target[prop] = value
return true
}
}
})
proxy.push('1') // -> TypeError: 'set' on proxy: trap returned falsish for property '0'
由此我们可以看出, 代理set方法, 需要返回**true**来表明值已经被成功设置, 否则将会触发TypeError
那么, 我们将return true直接提到if外再测试一下
const proxy = new Proxy([], {
set(target, prop, value) {
if (typeof value === 'number') {
target[prop] = value
}
return true
}
})
proxy.push('1')
console.log(proxy[0]) // -> undefined
console.log(proxy.length) //-> 1
has捕获器
has(target, p): boolean
target: 被代理对象p: 对象属性
该捕获器用于拦截
key in object
使用JSInfo的例子来说明
该例子构建一个语法糖, 使用in操作符来判断一个数字是否在范围内
let range = {
start: 1,
end: 10
}
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end
}
})
alert(5 in range); // true
alert(50 in range); // false
ownkeys捕获器
通过配置handler的ownkeys方法可以为对象的**获取属性列表(底层的[[OwnPropertyKeys]])**设置代理
ownkeys(target): Array<String|Symbol>
target: 被代理对象
该捕获器用于拦截:
Object.getOwnPropertyNames(obj): 返回对象所有非 Symbol 键Object.getOwnPropertySymbols(obj): 返回对象所有 Symbol 键Object.keys/values(): 返回对象带有enumerable标志的非 Symbol 键/值for..in: 循环遍历对象所有带有enumerable标志的非 Symbol 键,以及原型对象的键。
比如
- 不想让
_开头的属性被枚举出来
const obj = { a: 1, b: 2, _a: 3 }
const proxy = new Proxy(obj, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
for(let key in proxy) console.log(key) // -> a b
console.log(Object.getOwnPropertyNames(proxy)) // -> [ 'a', 'b' ]
console.log(Object.keys(proxy)) // -> [ 'a', 'b' ]
console.log(Object.values(proxy)) // -> [ 1, 2 ]
- 返回不存在的键, 需要搭配着
[getOwnPropertyDescriptor](###getOwnPropertyDescriptor%E6%8D%95%E8%8E%B7%E5%99%A8)使用
const obj = {}
const proxy = new Proxy(obj, {
ownKeys(target) {
return [ 'a', 'b' ]
},
getOwnPropertyDescriptor() {
// 所有属性均可枚举,配置,写入
return {
enumerable: true,
configurable: true,
writable: true,
}
}
})
console.log(Object.keys(proxy)) // -> [ 'a', 'b' ]
console.log(Object.values(proxy)) // -> [ undefined, undefined ]
console.log(Object.getOwnPropertyNames(proxy)) // -> [ 'a', 'b' ]
proxy.a = '1'
console.log(proxy.a) // -> 1, 如果writable设置为false, 则打印 undefined
console.log(obj) // -> {}, 不会反映到源对象, 可以设置set捕获器实现虚拟属性反映到源对象
getOwnPropertyDescriptor捕获器
getOwnPropertyDescriptor(target: {}, p: string | symbol): PropertyDescriptor
target: 被代理对象p: 对象属性
该捕获器用于拦截
[Object.getOwnPropertyDescriptor()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)
deleteProperty捕获器
deleteProperty(target, p): boolean
target: 被代理对象p: 对象待删除属性
该捕获器用于拦截
delete object[key]
注: 如果被代理对象的待删除的属性是不可配置的(
configurable: false), 则会抛出TypeError
const obj = { a: 1, b: 2 }
Object.defineProperty(obj, 'c', {
configurable: false,
enumerable: true,
value: 3
})
const proxy = new Proxy(obj, {
deleteProperty(target, p) {
console.log('删除属性:', p)
if (p === 'c') { // 只删除c属性
delete target[p]
}
return true // '假'删除成功, 相当于把对象的每个属性通过defineProperty定义为configurable: false一样
}
})
console.log('原始属性', Object.keys(proxy)) // -> [ 'a', 'b', 'c' ]
delete proxy.a // -> 删除属性: a
delete proxy.b // -> 删除属性: b
console.log('删除a, b后', Object.keys(proxy)) // -> 删除a, b后 [ 'a', 'b', 'c' ]
delete proxy.c // -> 删除属性: c
// -> TypeError: 'deleteProperty' on proxy: trap returned truish for property 'c' which is non-configurable in the proxy target
apply捕获器
apply(target, thisArg, args)
target: 被代理对象thisArg: this值args: 方法参数列表
该捕获器用于拦截方法的apply
比如我们来实现一个delay函数
function delay (fn, time) {
return new Proxy(fn, {
apply(target, thisArg, args) {
console.time('Delay')
setTimeout(() => {
target.apply(thisArg, args)
console.timeEnd('Delay')
}, time)
}
})
}
function test () {
console.log('test')
}
delay(test, 1000)() // -> test \n Delay: 1.001s
关联阅读:
- 如何实现一个准确的定时器
- apply, bind, call的区别以及用法
construct捕获器
construct(target, args): Object
target: 被代理对象args: 构造函数参数列表
比如, 捕获构造函数并为其设置固定参数值
function User (name, age) {
this.name = name
this.age = age
}
const UserProxy = new Proxy(User, {
construct(target, args) {
console.log('target is', target, 'args is', args) // -> target is [Function: User] args is [ 'jojo', 20 ]
args.length = 1
return new target(...args, 18)
}
})
console.log(new UserProxy('jojo', 20)) // -> User { name: 'test', age: 18 }
console.log(UserProxy.length) // -> 2, 构造函数接收的参数数量
撤销代理
语法:
let { proxy, revoke } = Proxy.revocable(target, handler)
- proxy: 目标对象的代理对象
- revoke: 关闭代理对象的函数(取消对象与代理对象间的连接)
例子
const user = {
name: 'howe'
}
const { proxy, revoke } = Proxy.revocable(user, {})
console.log(proxy.name) // howe
revoke()
console.log(proxy.name) // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
revoke之后, proxy的内置属性[[IsRevoked]]将会被置为true
我们可以通过一个
WeakMap来存储proxy和对应的revoke函数这样的好处是, 我们在有需要的时候能够通过proxy来找到对应的revoke函数, 而不需要一直携带revoke, 又因为使用的是
weakmap, 不会影响垃圾回收机制.
const revokes = new WeakMap()
const user = {
name: 'howe'
}
const { proxy, revoke } = Proxy.revocable(user, {})
revokes.set(proxy, revoke)
console.log(proxy.name) // howe
revokes.get(proxy)() // <- revoke
console.log(proxy.name) // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
总结
- 代理对象相当于在原对象外面包裹了一层对象, 在访问或对原对象操作的时候需要经过代理层进行相应处理, 如果没有对应处理方法则会直接使用原对象的方法进行处理.
- 代理对象也可以代理代理对象(套娃)
- IE浏览器不支持(应该没人用IE了吧)
- 代理对象会保持原对象类型
- Proxy的局限性
- 在代理JS内置对象(如:
Map,Set,Date,Promise等)的时候使用内置方法将会失败
- 在代理JS内置对象(如:
const map = new Map()
const proxy = new Proxy(map, {})
proxy.set('a', 1) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver #<Map>
解决方案:
- 私有字段问题
class User {
#name = 'howe'
getName () {
return this.#name
}
}
const user = new User()
const userP = new Proxy(user, {})
console.log(user.getName()) // howe
console.log(userP.getName()) // Uncaught TypeError: Cannot read private member #name from an object whose class did not declare it
解决方案同内置对象处理方法一样
- 通过
Proxy.revocable()方法来创建可撤销的代理