Free email newsletter: “ES.next News

2016-11-12

Pitfall: not all objects can be proxied transparently

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

Acknowlegement: Thanks to Allen Wirfs-Brock for pointing out the pitfall explained in this blog post.

No comments: