Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaScript - Proxy set vs. defineProperty

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?
like image 528
Matthew Layton Avatar asked Jun 13 '20 09:06

Matthew Layton


1 Answers

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.

like image 134
T.J. Crowder Avatar answered Sep 21 '22 21:09

T.J. Crowder