Lets say I have a class defined as follows:
class MyClass {
constructor(a, b) {
this._a = a;
this._b = b;
}
get a() {
return this._a;
}
set a(val) {
this._a = val;
}
add() {
return this._a + this._b;
}
}
I want to be able to access and manipulate the getter and setter functions directly at runtime in order to wrap them in additional debugging code. With the 'add' function, I can do this:
let oldAdd = MyClass.prototype.add;
MyClass.prototype.add = function() {
console.log('add called!');
let result = oldAdd.call(this);
console.log('add result: ' + result);
return result;
}
However, I cannot find a way to modify the getter and setter functions in a similar way.
I have tried
let propDef = Reflect.getOwnPropertyDescriptor(MyClass.prototype, 'a');
propDef.get = function() {
// ...
}
But this change does not actually get applied.
Any ideas?
I am also interested to know if it's possible to access and modify the constructor function in the same way.
Yes, you can do that by reconfiguring the properties. Here's an example (see comments):
class MyClass {
constructor(a, b) {
this._a = a;
this._b = b;
}
get a() {
return this._a;
}
set a(val) {
this._a = val;
}
add() {
return this._a + this._b;
}
}
// Use it before redefining
const instance = new MyClass(1, 2);
console.log(instance.a); // 1
instance.a = 2;
console.log(instance.a); // 2
// Redefine the property
const desc = Reflect.getOwnPropertyDescriptor(MyClass.prototype, "a");
const {get: originalGet, set: originalSet} = desc;
desc.get = function() {
const value = originalGet.call(this);
console.log("Debugging 'get' here, a's value is " + value);
return value;
};
desc.set = function(newValue) {
console.log("Debugging 'set' here, a's new value is " + newValue);
originalSet.call(this, newValue);
};
Object.defineProperty(MyClass.prototype, "a", desc);
// Use it after redefining
console.log(instance.a); // 2, after seeing console statement
instance.a = 3; // Triggers another console statement
console.log(instance.a); // 3 (w/ console statement)
The reason what you did didn't work was partially that you never set the new descriptor on the object. The descriptor you get back doesn't provide direct access to the definition within the object, it's just a new object created by getOwnPropertyDescriptor
. To make changes effective, you need to set the new descriptor.
You asked below about doing the same for MyClass
itself. As you pointed out, in addition to replacing the function, we need to be sure that its properties show up on the replacement for it.
A simple, easy way to do that is to make the new function inherit from the old:
const originalConstructor = MyClass;
MyClass = class MyClass extends originalConstructor {
constructor(...args) {
return new originalConstructor(...args);
}
};
It's a little-known fact that when B
extends A
, there are two things that happen:
B.prototype
's prototype is set to A.prototype
(this is commonly known), andB
's prototype is set to A
(less well-known)So any statics on A
are available on B
through inheritance.
So:
class Base {
feature() {}
static staticFeature() {}
}
class MyClass extends Base {
subFeature() {}
static staticSubFeature() {}
}
const originalConstructor = MyClass;
MyClass = class MyClass extends originalConstructor {
constructor(...args) {
return new originalConstructor(...args);
}
};
console.log(typeof MyClass.staticFeature);
console.log(typeof MyClass.staticSubFeature);
const instance = new MyClass();
console.log(typeof instance.feature);
console.log(typeof instance.subFeature);
Or if you want a really high-fidelity copy, you'd derive from MyClass
's prototype and then copy over any MyClass
own properties; but it's more complicated:
const originalConstructor = MyClass;
MyClass = class MyClass extends Object.getPrototypeOf(originalConstructor) {
constructor(...args) {
return new originalConstructor(...args);
}
};
Object.getOwnPropertyNames(originalConstructor)
.concat(Object.getOwnPropertySymbols(originalConstructor))
.forEach(name => {
MyClass[name] = originalConstructor[name];
});
Example:
class Base {
feature() {}
static staticFeature() {}
}
class MyClass extends Base {
subFeature() {}
static staticSubFeature() {}
}
const originalConstructor = MyClass;
MyClass = class MyClass extends Object.getPrototypeOf(originalConstructor) {
constructor(...args) {
return new originalConstructor(...args);
}
};
Object.getOwnPropertyNames(originalConstructor)
.concat(Object.getOwnPropertySymbols(originalConstructor))
.forEach(name => {
MyClass[name] = originalConstructor[name];
});
console.log(typeof MyClass.staticFeature);
console.log(typeof MyClass.staticSubFeature);
const instance = new MyClass();
console.log(typeof instance.feature);
console.log(typeof instance.subFeature);