15

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

  • New properties are defined.
  • Existing properties are changed.

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

  • proxy has a property name which is what I'd expect.
  • Changing the name property tells me that name has been defined; not what I'd expect.
  • Defining the age property tells me that age has been defined; as I'd expect.

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

  • proxy has a property name which is what I'd expect.
  • Changing the name property tells me that name has been changed; as I'd expect.
  • Defining the age property tells me that age has been changed; not what I'd expect.

Questions

  • Why does defineProperty catch property changes?
  • Why does the addition of set override defineProperty?
  • How do I get the proxy to correctly trap defineProperty for new properties and set for property changes?
user3840170
  • 26,597
  • 4
  • 30
  • 62
Matthew Layton
  • 39,871
  • 52
  • 185
  • 313

1 Answers1

15

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    (FWIW, I go into Proxy and traps and the surprising fact of the `defineProperty` trap getting triggered by setting data properties in Chapter 14 of my new book. Links in my profile if you're interested.) – T.J. Crowder Jun 13 '20 at 10:08
  • @Bergi - Gah, and I knew that, and I did it anyway. Not the first time, either. Thanks! Looking at it, I also had an unnecessary use of `getOwnPropertyDescriptor` (I was originally going to use the descriptor, then changed the code just to check for whether the prop existed. Fixed that, too. – T.J. Crowder Jun 13 '20 at 10:25
  • This is an excellent question and answer. I'm surprised it hasn't gotten more views given how important Proxy can be for things like setting up bindings. I thought I knew everything about Proxy but never saw this detail. Turns out it was the details of defineProperty I had never fully understood. Years of going through the MDN docs once in a while hadn't clued me into this. Thanks for a real lesson this morning. – WeakPointer Jul 14 '20 at 14:51