In a nutshell: with direct proxies, one can no longer write a caretaker proxy that nulls out a pointer to its target once revoked. This means that the target can no longer be garbage-collected separately from its proxy.
As pointed out by David Bruant, caretakers are often useful precisely for memory management, where one uses revocation of the caretaker to release the underlying resources, as described here.
Such caretakers were easy to define in the old Proxy design, since the link between a proxy and its target was fully under the control of the handler (i.e. fully virtual). However, with direct proxies, the proxy has a built-in, immutable reference directly to its target. This reference can’t be modified or nulled out.
Provide an extension to the Proxy API that allows scripts to null out the proxy-target reference. Of course, the API must be designed with care so that this link can’t be nulled out by any old client of the proxy. Only the creator of the proxy should have the right to revoke it.
Because revocable proxies appear to be a niche abstraction, we propose to introduce a separate Proxy constructor dedicated to creating revocable proxies. Proxies created with the regular Proxy constructor would remain unrevocable.
Proposal:
Proxy.revocable.Proxy.revocable(target, handler) returns an object {proxy: proxy, revoke: revoke}.revoke is a zero-argument function that, when called, revokes its associated proxy.TypeError.
Revoking a proxy is observably equivalent to replacing the handler such that all handler traps throw a TypeError unconditionally.
Example:
let { proxy, revoke } = Proxy.revocable(target, handler); proxy.foo // traps revoke() // always returns undefined proxy.foo // throws TypeError: "proxy is revoked"
Revocable proxies reintroduce a subtle issue from the old design that had to do with fixing a proxy while it still has pending trap activations on the runtime stack: an unrevoked proxy may call one of its traps and be revoked by the time the trap returns:
let { proxy, revoke } = Proxy.revocable({foo:42}, { get: function(target, name) { revoke(); return target[name]; } }); proxy.foo // traps and revokes the proxy
A first question to answer is whether the above proxy.foo call should return 42 or should instead throw TypeError: proxy is revoked. We propose to have the trap still return its result since the proxy was still unrevoked at the time the operation was performed.
For invariant enforcement to work correctly, the trap’s result must be verified against the original target. So, the proxy must ensure that it holds on to the original target before calling the trap, and use that stored pointer after the trap returns.
Proxy.revocable(target, handler)
[[Call]]
When the [[Call]] method of a revoke function F is called:
The specification of operations that trap on a proxy P would follow the following template:
Checking for a null [[Handler]] works because a valid handler must always be of type Object (it can’t normally be set to null).
An IsCallable test on a revoked proxy should return false.
The typeof operator, applied on a revoked proxy, should continue to return the same result it returned for the unrevoked proxy (ensuring typeof remains a stable operator).
Since the instanceof operator tries to access the [[Prototype]] of its LHS and [[Get]]s the “prototype” of its RHS, if either LHS or RHS is a revoked proxy, a TypeError will be thrown.