javascriptobjectes6-proxyobject-properties

JavaScript - Proxy set vs. defineProperty


I want to build a proxy that detects changes to an object:

Code Sample 1 - defineProperty

const me = {
  name: "Matt"
}

const proxy = new Proxy(me, {
  defineProperty: function(target, key, descriptor) {
    console.log(`Property ${key} defined.`);
    return Object.defineProperty(target, key, descriptor);
  }
});

proxy // { name: 'Matt' }

proxy.name = "Mark";
// Property name defined.
// Mark

proxy.age = 20;
// Property age defined.
// 20

Code Sample 1 - Observations

Code Sample 2 - set

const me = {
  name: "Matt"
}

const proxy = new Proxy(me, {
  defineProperty: function(target, key, descriptor) {
    console.log(`Property ${key} defined.`);
    return Object.defineProperty(target, key, descriptor);
  },
  set: function(target, key, value) {
    console.log(`Property ${key} changed.`);
    return target[key] = value;
  }
});

proxy // { name: 'Matt' }

proxy.name = "Mark";
// Property name changed.
// Mark

proxy.age = 20;
// Property age changed.
// 20

Code Sample 2 - Observations

Questions


Solution

  • Why does defineProperty catch property changes?

    Because when you change a data property (as opposed to an accessor), through a series of specification steps it ends up being a [[DefineOwnProperty]] operation. That's just how updating a data property is defined: The [[Set]] operation calls OrdinarySet which calls OrdinarySetWithOwnDescriptor which calls [[DefineOwnProperty]], which triggers the trap.

    Why does the addition of set override defineProperty?

    Because when you add a set trap, you're trapping the [[Set]] operation and doing it directly on the target, not through the proxy. So the defineProperty trap isn't fired.

    How do I get the proxy to correctly trap defineProperty for new properties and set for property changes?

    The defineProperty trap will need to differentiate between when it's being called to update a property and when it's being called to create a property, which it can do by using Reflect.getOwnPropertyDescriptor or Object.prototype.hasOwnProperty on the target.

    const me = {
      name: "Matt"
    };
    
    const hasOwn = Object.prototype.hasOwnProperty;
    const proxy = new Proxy(me, {
      defineProperty(target, key, descriptor) {
        if (hasOwn.call(target, key)) {
          console.log(`Property ${key} set to ${descriptor.value}`);
          return Reflect.defineProperty(target, key, descriptor);
        }
        console.log(`Property ${key} defined.`);
        return Reflect.defineProperty(target, key, descriptor);
      },
      set(target, key, value, receiver) {
        if (!hasOwn.call(target, key)) {
          // Creating a property, let `defineProperty` handle it by
          // passing on the receiver, so the trap is triggered
          return Reflect.set(target, key, value, receiver);
        }
        console.log(`Property ${key} changed to ${value}.`);
        return Reflect.set(target, key, value);
      }
    });
    
    proxy; // { name: 'Matt' }
    
    proxy.name = "Mark";
    // Shows: Property name changed to Mark.
    
    proxy.age = 20;
    // Shows: Property age defined.

    That's a bit off-the-cuff, but it'll get you heading the right direction.

    You could do it just with a set trap, but that wouldn't be fired by any operation that goes direct to [[DefineOwnProperty]] rather than going through [[Set], such as Object.defineProperty.