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 propertyname
which is what I'd expect.- Changing the
name
property tells me thatname
has been defined; not what I'd expect. - Defining the
age
property tells me thatage
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 propertyname
which is what I'd expect.- Changing the
name
property tells me thatname
has been changed; as I'd expect. - Defining the
age
property tells me thatage
has been changed; not what I'd expect.
Questions
- Why does
defineProperty
catch property changes? - Why does the addition of
set
overridedefineProperty
? - How do I get the proxy to correctly trap
defineProperty
for new properties andset
for property changes?
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 propertyname
which is what I'd expect.- Changing the
name
property tells me thatname
has been defined; not what I'd expect. - Defining the
age
property tells me thatage
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 propertyname
which is what I'd expect.- Changing the
name
property tells me thatname
has been changed; as I'd expect. - Defining the
age
property tells me thatage
has been changed; not what I'd expect.
Questions
- Why does
defineProperty
catch property changes? - Why does the addition of
set
overridedefineProperty
? - How do I get the proxy to correctly trap
defineProperty
for new properties andset
for property changes?
1 Answer
Reset to default 16Why 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
.