先记住这个答案
用 Proxy.revocable(target, handler) 创建,它不返回代理本身,而是返回 { proxy, revoke }。平时使用 proxy 和普通 Proxy 没有区别。一旦调用 revoke(),这个代理就和原对象断开了,之后对它做任何会触发内部操作的事,比如读属性、赋值、in、delete、Object.keys,都会抛出 TypeError。撤销是一次性的,不能恢复,原对象本身不受影响。
Proxy.revocable返回代理与撤销函数- 撤销后几乎所有操作都抛
TypeError - 撤销后仍可比较代理引用
- 撤销不可逆,重复调用
revoke()没有效果
撤销前后对比
先看一段完整的对比,重点是撤销之后哪些操作会报错、哪些不会。
const target = { name: 'Tom' }
const { proxy, revoke } = Proxy.revocable(target, {})
console.log(proxy.name) // 撤销前正常转发:Tom
revoke()
const tryIt = (label, fn) => {
try { fn(); console.log(label, 'ok') }
catch (err) { console.log(label, err.name) }
}
tryIt('get', () => proxy.name)
tryIt('set', () => { proxy.age = 18 })
tryIt('in', () => 'name' in proxy)
tryIt('keys', () => Object.keys(proxy))
console.log(typeof proxy, proxy === proxy) // 不触发内部方法
console.log(target.name) // 原对象不受影响
revoke() // 再调一次不会报错,也没有任何效果查看输出与解释
Tom
get TypeError
set TypeError
in TypeError
keys TypeError
object true
Tom注意 handler 是空对象,一个 trap 都没写,撤销后照样全部报错。报错不是 trap 抛的,而是引擎在执行代理的内部方法前先检查代理有没有被撤销。typeof 和 === 只看值本身,不会调用代理的内部方法,所以还能正常用。
它解决什么问题:临时授权
普通对象一旦交出去,你就没法再收回来。别人只要存了一份引用,想什么时候读就什么时候读。可撤销代理就是给这种场景准备的:你交出去的不是原对象,而是一个随时能作废的代理。
比如一个插件系统,主程序把配置对象通过代理交给插件,插件被卸载时调用 revoke(),插件里残留的定时器、闭包再想读配置,就会直接报错,而不是悄悄读到最新的数据。再比如只允许某段代码在 5 秒内访问一份数据,到时间用 setTimeout 调一次 revoke() 就行。
这里有个限制要说清楚。代理只管住它自己这一层。如果插件在撤销前执行过 const db = proxy.db,拿到的 db 是原对象里的嵌套对象本身,不是代理,撤销以后它还能继续用。要把整棵对象树都管住,需要在 get trap 里把返回的对象也包成可撤销代理,并记下所有 revoke,统一撤销。这种做法叫「隔离膜」(membrane),一些沙箱方案就是这么实现的。
撤销对内存的影响
revoke() 执行后,代理内部保存的 target 和 handler 引用会被清掉。所以即使代理对象本身还被别人拿着,只要原对象没有其他引用,它也能被垃圾回收。普通 Proxy 做不到这一点,只要代理活着,原对象就一直活着。
容易答错的地方
- 以为撤销会影响原对象
- 撤销只作用在代理上。原对象
target还是普通对象,直接访问它完全正常。想让原对象也不能改,那是Object.freeze的事。 - 以为撤销后所有操作都报错
typeof proxy返回'object',proxy === proxy返回true,把它作为WeakMap的键也可以,因为这些操作只用到对象的身份,不会调用内部方法。但像Array.isArray(proxy)这样需要看穿代理去检查原对象的操作,撤销后同样会抛TypeError。- 以为能通过
new Proxy再加一个开关实现同样效果 - 可以在每个 trap 里判断一个
revoked标记来模拟,但要把十几个 trap 都写一遍,而且原对象引用一直留在闭包里,没法被回收。原生的Proxy.revocable更完整。
面试官还会怎么问?
撤销之后还能恢复吗?
不能。revoke() 是单向的,想重新授权只能再调一次 Proxy.revocable 创建一个新的代理,交给对方新的引用。
撤销后报错的时机是什么时候?
是在下一次操作代理的时候,不是调用 revoke() 的那一刻。revoke() 本身只是把内部引用清空,谁再去读写这个代理,谁就收到 TypeError。
Vue 3 的响应式用了可撤销代理吗?
没有。Vue 3 用的是普通的 new Proxy,它需要的是拦截读写做依赖收集和触发更新,不需要收回访问权限。可撤销代理更多出现在沙箱、插件隔离这类安全场景。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。