class MyObject {
#example;
prop;
setProp = (val) => (this.prop = val);
get example() {
return this.#example;
}
set example(newValue) {
this.#example = newValue;
}
}
function attachChangeListener(obj, onChange) {
// In this example, we'll only worry about _own_ properties,
// and only ones with string names, but you can adapt as
// appropriate
for (const name of Object.getOwnPropertyNames(obj)) {
const descr = Object.getOwnPropertyDescriptor(obj, name);
if ("value" in descr) {
// It's a data property, turn it into an accessor property
let value = obj[name];
Object.defineProperty(obj, name, {
get() {
return value;
},
set(newValue) {
value = newValue;
onChange(obj, name, newValue);
},
enumerable: descr.enumerable,
// You can set `configurable to `false`, but then if the code
// for the object does reconfigure the property, that code
// will fail.
configurable: true,
});
} else {
// It's an accessor, insert ourselves in it
const { get, set } = descr;
Object.defineProperty(obj, name, {
get() {
return get.call(obj);
},
set(newValue) {
set.call(obj);
onChange(obj, name, newValue);
},
enumerable: descr.enumerable,
// See note about `configurable` above
configurable: true,
});
}
}
// For `MyObject`, we'd probably want to at least check the immediate prototype (if
// not the entire chain) to catch accessors defined on `MyObject.prototype` rather
// that directly on the object.
const proto = Object.getPrototypeOf(obj);
if (proto) {
for (const name of Object.getOwnPropertyNames(proto)) {
const descr = Object.getOwnPropertyDescriptor(proto, name);
if (!("value" in descr)) {
// It's an accessor, override it with our own on the object
const { get, set } = descr;
Object.defineProperty(obj, name, {
get() {
return get.call(obj);
},
set(newValue) {
set.call(obj);
onChange(obj, name, newValue);
},
enumerable: descr.enumerable,
// See note about `configurable` above
configurable: true,
});
}
}
}
// You'd probably *also* add your proxy here
return obj;
}
const inst = attachChangeListener(new MyObject(), function (obj, name, value) {
console.log(`**Change detected** ${name} changed to ${value}`);
});
// You'll be notified of this change
inst.setProp("x");
// And of this one
inst.example = 42;
// But not this one
Object.defineProperty(inst, "example", {
value: "silent change",
writable: true,
enumerable: true,
configurable: true,
});
console.log(`example = ${inst.example}`);
// And not this addition
inst.newProperty = "added";
console.log(`newProperty = ${inst.newProperty}`);