Search code examples
javascriptnode.jsecmascript-6proxy-pattern

Apparent pollution with multiple Proxy objects for the same target


I'm attempting to create multiple Proxy wrappers for the same target object in JavaScript, with each individual wrapper having slightly different properties which affect how the wrapped functionality operates. These properties are assigned to and accessed from the receiver object in the set and get handlers. However, when I examine the generated Proxies, all of them have the property set I expect to have been assigned to the last Proxy created.

const obj = {};

const proxies = ['one', 'two'].map(name => {
  console.log(`proxy ${name}`);

  const proxy = new Proxy(obj, {
    get: (target, prop, receiver) => {
      if (prop === 'name') { return receiver.name; }

      return target[prop];
    },
    set: (target, prop, val, receiver) => {
      if (prop === 'name') {
        console.log(`setting name ${val} on receiver`);
        Object.defineProperty(receiver, prop, {
            value: val,
            configurable: true,
            enumerable: true}
        );
      } else {
        console.log(`setting ${prop} ${val} on target`);
        target[prop] = val;
      }

      return true;
    }
  });

  proxy.name = name;

  return proxy;
});

console.log();
console.log(proxies);

My expected result: [{name: 'one'}, {name: 'two'}].

The actual result: [{name: 'two'}, {name: 'two'}]. Even though they appear identical, they are not strictly equal.

If I omit const obj and create my objects with new Proxy({}, ...) I get the expected result -- a proxy one and a proxy two, presumably since the target reference is not shared between them. So: what on earth? From my understanding, using the receiver to store name should prevent it from being propagated to the target object, yet it seems to be doing so anyway.


Solution

  • Your snippet

    Object.defineProperty(receiver, prop, {
        value: val,
        configurable: true,
        enumerable: true}
    );
    

    isn't going to do what (I think) you expect it to do. Since receiver here is the proxy object, the property definition will also be proxied through to target, meaning that the distinction between the branches in your if/else is almost nothing. If you're looking to store a unique name for each proxy object, the easiest thing to do in this case would be to use the closure's scope, e.g.

    const proxies = ['one', 'two'].map(name => {
      console.log(`proxy ${name}`);
    
      const proxy = new Proxy(obj, {
        get: (target, prop, receiver) => {
          if (prop === 'name') { return name; }
    
          return Reflect.get(target, prop, receiver);
        },
        set: (target, prop, val, receiver) => {
          if (prop === 'name') {
            name = val;
            return true;
          }
    
          return Reflect.set(target, prop, val, receiver);
        },
        ownKeys: (target) => {
          return Reflect.ownKeys(target).concat('name');
        },
        getOwnPropertyDescriptor: (target, prop) => {
          if (prop === "name") return { enumerable: true, writable: true, configurable: true, value: name };
    
          return Reflect.getOwnPropertyDescriptor(target, prop);
        },
      });
    
      return proxy;
    });