Reputation: 15
I have some code where I want to dynamically substitute a function, and I thought about using idiom used in ms-dos interrupts (coming from C++/asm background to JS). So I wrote a code snippet which works... but not if the function uses anything referenced by 'this'. How to make it work with this-vars and if it's also a prototype function. What is the name for that idiom?
googling "method chaining" refers to another unrelated non-notable thing.
function patient(a,s,d) { /*do something*/ }
....
var oldFunc = patient;
patient = function(a,s,d) {
if(a==something) oldFunc(a,s,d); else { /* do something*/ }
}
Upvotes: 1
Views: 80
Reputation: 29086
If you are trying to override a function but lose the this
context, there are few ways around this. Consider the following simple example.
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const myAlarm = new Alarm();
myAlarm.setTime("12:00").setEnabled(true);
console.log(myAlarm.toString());
In this case, everything works, because this
is never tampered with, so it's what we expect every time. Here is what happens if we try naively overriding the setTime
method:
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const myAlarm = new Alarm();
//let's override something
const oldSetTime = myAlarm.setTime;
myAlarm.setTime = function(time) {
console.log("overriden method!");
return oldSetTime(time); //this will lose the context of "this"
}
myAlarm.setTime("12:00").setEnabled(true);//error because "this" is undefined
console.log(myAlarm.toString());
So, the naive way doesn't work. There are several ways to go around losing the context.
Function#bind
When you bind a function, you actually create a new one where the this
context is permanently set to something. It's called a "bound function".
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const myAlarm = new Alarm();
const oldSetTime = myAlarm.setTime.bind(myAlarm); //bind a function to a context permanently
myAlarm.setTime = function(time) {
console.log("overriden method!");
return oldSetTime(time);
}
myAlarm.setTime("12:00").setEnabled(true);
console.log(myAlarm.toString());
Function#apply
or Function#call
Both are very similar. In both cases you will execute a function and supply the value of the this
context. You can then supply any additional parameters to the function to execute with. .call()
will just take any amount of parameters and forward them on, while .apply()
requires only one parameter that is array-like and would be turned into the arguments
for the function executed.
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const myAlarm = new Alarm();
const oldSetTime = myAlarm.setTime;
myAlarm.setTime = function(time) {
console.log("overriden method!");
return oldSetTime.call(this, time);
}
myAlarm.setTime("12:00").setEnabled(true);
console.log(myAlarm.toString());
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const myAlarm = new Alarm();
const oldSetTime = myAlarm.setTime;
myAlarm.setTime = function() {
console.log("overriden method!");
return oldSetTime.apply(this, arguments);
}
myAlarm.setTime("12:00").setEnabled(true);
console.log(myAlarm.toString());
The .apply()
approach is usually more scalable since you just forward the arguments
originally executed with. This way if the original function changes signature, you don't actually care and don't need to change anything. Let's say it's now setTime(hours, minutes)
- the forward to the original would still work. While if you use .call()
, you need to do a bit more work - you'd need to go and change the parameters passed passed and you would need to modify the entire override to something like
myAlarm.setTime = function(hours, minutes) {//you need to know what the function takes
console.log("overriden method!");
return oldSetTime.call(this, hours, minutes); //so you can pass them forward
}
Although you can get around that by using spread syntax
myAlarm.setTime = function() {//ignore whatever is passed in
console.log("overriden method!");
return oldSetTime.call(this, ...arguments); //spread the arguments
}
in which case the outcome of both .apply(this, arguments)
and .call(this, ...arguments)
becomes identical but requires a slight amount of planning ahead.
Instead of modifying the object, you can set up a proxy that intercepts and possibly modifies calls. This could be an overkill in some cases or just what you need. Here is a sample implementation that overrides all method calls
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const allMethodsHandler = {
get(target, propKey) {
const origMethod = target[propKey];
return function() {
const result = origMethod.apply(target, arguments); //you can also use .call(target, ...arguments)
console.log(`called overriden method ${propKey}`);
return result;
};
}
};
const myAlarm = new Alarm();
myOverridenAlarm = new Proxy(myAlarm, allMethodsHandler);
myOverridenAlarm
.setTime("12:00")
.setEnabled(true); //you get no log!
console.log(myOverridenAlarm.toString());
However, care has to be taken. As you can see, the call to setEnabled
does not produce a log. This is because it doesn't go through the proxy - setTime
returns the original object rather than the proxy. I've left this in to showcase a problem. Overriding all is sometimes too powerful. In this case, there would be a problem if you want to fetch myOverridenAlarm.time
, for example as it would still go through the handler and treat that as a method. You can modify the handler to check for methods, maybe even check if the result is the same object (fluid interface) and wrap it in a proxy, or return the current proxy as appropriate but it gets a bit cumbersome. It also depends on your use-case.
Something simpler is overriding a single method through a proxy. It's a very similar concept to using either .bind
or .call
or .apply
but it's more reusable in some respects.
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const singleMethodHandler = {
apply(targetMethod, thisArg, ...args) { //collect the rest of the arguments into "args" to pass on
console.log(`overriden method!`);
const result = targetMethod.apply(thisArg, args);
return result;
}
};
const myAlarm = new Alarm();
//override setTime with a proxied version
myAlarm.setTime = new Proxy(myAlarm.setTime, singleMethodHandler);
myAlarm.setTime("12:00").setEnabled(true);
console.log(myAlarm.toString());
This is a more lightweight version because you don't override all methods current and future, so it's much more manageable. Also, it's reusable - you can just add myAlarm.setEnabled = new Proxy(myAlarm.setEnabled, singleMethodHandler);
and you'd get the same functionality there. So if you only need to selectively override methods with the same functionality (in this case logging), then this is easy to do. However, it does mean changing the object.
If you want to avoid changing the instance and prefer to have the same thing applied across all instances, then you can change the prototype of the object so any call to the method will use a proxied version:
class Alarm {
setTime(time) {
this.time = time;
return this;
}
setEnabled(enabled) {
this.enabled = enabled;
return this;
}
toString() { return `time: ${this.time}\nenabled: ${this.enabled}`}
}
const singleMethodHandler = {
apply(targetMethod, thisArg, ...args) { //collect the rest of the arguments into "args" to pass on
console.log(`overriden method called with: "${args}"`);
const result = targetMethod.apply(thisArg, args);
return result;
}
};
//changing prototype before making a new isntance
Alarm.prototype.setTime = new Proxy(Alarm.prototype.setTime, singleMethodHandler);
const myAlarm = new Alarm();
//changing the prototype after making a new instance
Alarm.prototype.setEnabled = new Proxy(Alarm.prototype.setEnabled, singleMethodHandler);
myAlarm.setTime("12:00").setEnabled(true); //we get logs both times
console.log(myAlarm.toString());
Upvotes: 0
Reputation: 386680
You could use Function#bind
for binding this
to the new function.
function patient(a, s, d) { /*do something*/ }
// ....
var oldFunc = patient,
victim = function(a, s, d) {
if (a == something) oldFunc(a, s, d); else { /* do something*/ }
}.bind(this);
Upvotes: 2