Search code examples
javascriptxmlhttprequestecmascript-6

Is it possible to change a Proxy's target?


I have a class that implements the XMLHttpRequest interface. Depending on the URL passed to open(), I can determine whether to use the default XMLHttpRequest or my custom implementation. My idea is to use a proxy to do this:

let xhr = new XHRProxy();
xhr.open('GET', 'http://blah'); // Decide here depending on URL

I did some tests using the ES6 Proxy, which seems promising, but unfortunately the proxy target cannot be modified after constructing the Proxy:

var foo = {
    name() {
        return "foo";
    }
};
var bar = {
    name() {
        return "bar";
    }
}
var handler = {
    get(target, property, receiver) {
        if (property === "switchToBar") {
            // FIXME: This doesn't work because a Proxy's target is not exposed AFAIK
            receiver.target = bar;
            return function() {};
        } else {
            return target[property];
        }
    }
}
var proxy = new Proxy(foo, handler);
console.log(proxy.name()); // foo
proxy.switchToBar();
console.log(proxy.name()); // foo  :(

I think I can accomplish what I want by not setting a target at all - instead defining all traps to delegate to the desired object - but I'm hoping for a simpler solution.


Solution

  • Here's a go at "defining all traps to delegate to desired object"

    (function () {
      let mutableTarget;
      let mutableHandler;
    
      function setTarget(target) {
        if (!(target instanceof Object)) {
          throw new Error(`Target "${target}" is not an object`);
        }
        mutableTarget = target;
      }
    
      function setHandler(handler) {
        Object.keys(handler).forEach(key => {
          const value = handler[key];
    
          if (typeof value !== 'function') {
            throw new Error(`Trap "${key}: ${value}" is not a function`);
          }
    
          if (!Reflect[key]) {
            throw new Error(`Trap "${key}: ${value}" is not a valid trap`);
          }
        });
        mutableHandler = handler;
      }
    
      function mutableProxyFactory() {
        setTarget(() => {});
        setHandler(Reflect);
    
        // Dynamically forward all the traps to the associated methods on the mutable handler
        const handler = new Proxy({}, {
          get(target, property) {
            return (...args) => mutableHandler[property].apply(null, [mutableTarget, ...args.slice(1)]);
          }
        });
    
        return {
          setTarget,
          setHandler,
          getTarget() {
            return mutableTarget;
          },
          getHandler() {
            return mutableHandler;
          },
          proxy: new Proxy(mutableTarget, handler)
        };
      }
    
      window.mutableProxyFactory = mutableProxyFactory;
    })();
    
    const { 
      proxy, 
      setTarget 
    } = mutableProxyFactory();
    
    setTarget(() => 0);
    console.log(`returns: ${proxy()}`);
    
    setTarget({ val: 1 });
    console.log(`val is: ${proxy.val}`);
    
    setTarget({ val: 2 });
    console.log(`val is: ${proxy.val}`);
    
    setTarget(() => 3);
    console.log(`returns: ${proxy()}`);
    

    I feel like there must be some reason this isn't supported out of the box, but I don't have enough information to comment on that further.

    After hacking on this for a while, I've observed a few things. It seems the original target that the proxy constructor is called with is treated as part of the proxy's identity regardless. Setting the original target to a plain object and swapping the target to a function later raises an error, when the proxy is called. There are definitely some rough edges to this, so use with caution.