7

I can take a Javascript object o and create a new Proxy object from it:

let p = new Proxy(object, { ... })

But is there a way to mutate an existing object reference to track changes on the original object? In particular, is there a way I can track the addition of new keys on the object from exterior sources?

user3840170
  • 26,597
  • 4
  • 30
  • 62
yawn
  • 422
  • 1
  • 5
  • 21

2 Answers2

5

The Proxy spec supports defining a proxy on the prototype of an object as a means for inspecting actions on that object when they do not exist on the instance. While this isn't full parity with .watch() it does allow for your mentioned use case of knowing when new properties are added. Here is an example, with comments about caveats...

  // assuming some existing property you didn't create...
  const t = { existing: true };

  // proxy a new prototype for that object...
  const ctr = {};
  Object.setPrototypeOf(t, new Proxy(ctr, {
    get(target, key) {
      console.log('icu get');
      return Reflect.get(target, key) || ctr[key];
    },
    set(target, key, val) {
      console.log('icu set');
      // setting this container object instead of t keeps t clean, 
      // and allows get access to that property to continue being 
      // intercepted by the proxy
      Reflect.set(ctr, key, val);
      return true;
    },
    deleteProperty(target, key) {
      console.log('icu delete');
      delete ctr[key];
      return true;
    }
  }));

  // existing properties don't work
  console.log('existing');
  t.existing; // <nothing>
  t.existing = false; // <nothing>

  // new properties work
  console.log('new');
  t.test; // icu get
  t.test = 4; // icu set
  console.log(t.test); // icu get
                       // 4

  // but this doesn't work (and I think it should be a bug)
  console.log('delete');
  delete t.test; // icu get
                 // <missing icu delete>
  console.log(t.test); // 4
Marcus Pope
  • 2,293
  • 20
  • 25
  • Love it, great hack. I'm only interested in this for prototyping anyway, and so I can probably do something crazy like delete the keys on the existing object, assign them to the prototype proxy, and monkey patch Object.keys and friends to lie and include the moved properties in their output. – yawn Jul 13 '19 at 03:13
  • 1
    Also I think ctr should be `Object.create(Object.getPrototypeOf(t))`, otherwise the prototype chain is overwritten. – yawn Jul 13 '19 at 03:14
  • @yawn indeed, thanks! Although, i am unable to use the defineProperty trap with this hack :-/ – diegocr May 25 '20 at 18:15
  • @diegocr Me neither, looks like defineProperty directly updates the object (it doesn't look up the prototype chain, maybe a bug?). All other traps seem to work fine, even getOwnProperty etc. You could always money patch defineProperty, or write your other handlers to also look into the object directly. – yawn Jun 01 '20 at 19:10
2

Just create the object first and keep a reference to it before creating its Proxy.

Now you can modify either of them (the original object or its Proxy) and the other will also receive the changes unless you prevent them on the Proxy:

const o = {};
const p = new Proxy(o, {
  set: function(obj, prop, value) {
    if (prop === 'd') {
      return false;
    }
    
    obj[prop] = value;
    
    return true;
  },
});

// These operations are forwarded to the target object o:
p.a = 0;
p.b = 1;

// This one is prevented by the Proxy:
p.d = true;

// Both will have two properties, a and b:
console.log(o);

// You can also mutate the original object o and the Proxy will also get those changes:
o.c = false;

// Note that now the Proxy setter is not called, so you can do:
o.d = true;

// But the Proxy still gets the change:
console.log(p);

If you want to be notified when a new property is added, deleted or modified on an object without the possiblity that the original reference is used to mutate the original object directly, the only option you have is to create that object directly as a Proxy or overwrite the original one:

// Created from an empty object without a reference to it:
// const p = new Proxy({}, { ... });

// Overwrite the original reference:
let myObject = { a: 1, b: 2 };

myObject = new Proxy(myObject, {
  set: function(obj, prop, value) {
    if (prop in obj) {
      console.log(`Property ${ prop } updated: ${ value }`);
    } else {
      console.log(`Property ${ prop } created: ${ value }`);
    }

    obj[prop] = value;

    return true;
  },
  
  deleteProperty(obj, prop) {
    console.log(`Property ${ prop } deleted`);
  
    delete obj[prop];
  }
});

// Now there's no way to access the original object we
// passed in as the Proxy's target!

myObject.a = true;
myObject.a = false;
delete myObject.a;

There used to be an Object.prototype.watch(), but it has been deprecated.

Danziger
  • 19,628
  • 4
  • 53
  • 83
  • The problem is precisely that I'd like to be notified of changes on the underlying object. If I understand correctly there is no perfect solution for this at this time? (unfortunate for Object.prototype.watch getting removed!) – yawn Aug 27 '18 at 16:21
  • 1
    The only thing you can do is create the proxy with `const p = new Proxy({}, ...)` so that the only reference that you have is the one to the Proxy itself, so there are no references to the target object you might use to modify it directly. If you already have that reference, maybe because you are creating the Proxy with an existing object, then you could overwrite it: `foo = new Proxy(foo, { ... })`. – Danziger Aug 27 '18 at 16:32
  • 1
    @yawn For new properties only, you can achieve what you're asking. See my answer below. – Marcus Pope Jul 12 '19 at 19:42