Dmitry Anisimov
Dmitry Anisimov

Reputation: 15

On-the-fly replacement of function

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

Answers (2)

VLAZ
VLAZ

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.

Proxy

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

Nina Scholz
Nina Scholz

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

Related Questions