An ES6 proxy lets you intercept and customize operations performed on an object, its so-called target. Interception and customization is handled via a handler object (think even listener). Operations are intercepted by handler methods. If a handler method is missing, the corresponding operation is simply forwarded to the target.
Therefore, if the handler is the empty object, the proxy should transparently wrap the target. Alas, that doesn’t always work, as this blog post explains.
The problem
The following code demonstrates that instances of Date
can’t be proxied:
const target = new Date(); const handler = {}; const proxy = new Proxy(target, handler); proxy.getDate(); // TypeError: this is not a Date object.
The problem is that most built-in constructors have instances that have so-called internal slots. These slots are property-like storage associated with instances. The specification handles these slots as if they were properties whose names are wrapped in square brackets. For example:
O.[[GetPrototypeOf]]()
However, access to them does not happen via normal “get” and “set” operations, which is why it can’t be proxied. For example, a method proxy.foo()
invoked on a proxy will be forwarded to the built-in method target.foo()
. That method examines its this
and can’t find the internal slots, because this === proxy
.
For Date
methods, the language specification states:
Unless explicitly stated otherwise, the methods of the Number prototype object defined below are not generic and the
this
value passed to them must be either a Number value or an object that has a[[NumberData]]
internal slot that has been initialized to a Number value.
Other objects that can’t be proxied transparently
Whenever an object associates information with this
via a mechanism that is not controlled by proxies, you have the same problem.
For example, the following class Person
stores private information in the WeakMap _name
(more information on this technique):
const _name = new WeakMap(); class Person { constructor(name) { _name.set(this, name); } get name() { return _name.get(this); } }
Instances of Person
can’t proxied transparently:
> const jane = new Person('Jane'); > jane.name 'Jane' > const proxy = new Proxy(jane, {}); > proxy.name undefined
Arrays can be proxied
In contrast to other built-ins, Arrays can be proxied:
> const p = new Proxy(new Array(), {}); > p.push('a'); > p.length 1 > p.length = 0; > p.length 0
The reason for Array being proxyable is that, even though property access is customized to make length
work, they don’t have internal slots.
A work-around
As a work-around, you can change how the handler forwards method calls and selectively set this
to the target and not the proxy:
const handler = { get(target, propKey, receiver) { if (propKey === 'getDate') { return target.getDate.bind(target); } return Reflect.get(target, propKey, receiver); }, }; const proxy = new Proxy(new Date('2020-12-24'), handler); proxy.getDate(); // 24
The drawback of this approach is that none of the operations that the method performs on this
go through the proxy.
Further reading
- Comprehensive introduction to ES6 proxies: chapter “Metaprogramming with proxies” in “Exploring ES6”.
Acknowlegement: Thanks to Allen Wirfs-Brock for pointing out the pitfall explained in this blog post.
No comments:
Post a Comment