动态替换功能

时间:2019-01-14 12:11:51

标签: javascript oop

我有一些代码要动态替换一个函数,并且我考虑过使用ms-dos中断(从C ++ / asm背景到JS)中使用的惯用法。所以我写了一个有效的代码片段,但是...如果该函数使用了'this'所引用的任何内容,则无效。如何使其与this-vars一起使用以及它是否也是原型函数。这个成语叫什么名字?

搜索“方法链接”是指另一种无关的非显着事物。

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*/ }
}

2 个答案:

答案 0 :(得分:2)

您可以使用Function#bindthis绑定到新功能。

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);

答案 1 :(得分:0)

如果您尝试覆盖某个函数但丢失了this上下文,则解决此问题的方法很少。考虑下面的简单示例。

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());

在这种情况下,一切正常,因为this从未被篡改,因此我们每次都希望这样做。如果我们尝试天真的覆盖setTime方法,就会发生以下情况:

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());

因此,幼稚的方法行不通。有几种方法可以解决上下文丢失的问题。

Function#bind

绑定函数时,实际上创建了一个新的函数,其中this上下文被永久设置为某种东西。它称为“绑定函数”。

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#applyFunction#call

两者非常相似。在这两种情况下,您都将执行一个函数并提供this上下文的值。然后,您可以向函数提供任何其他参数来执行。 .call()将只接受任意数量的参数并将其转发,而.apply()仅需要一个一个参数,该参数类似于数组,并将被转换为arguments对于执行的功能。

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());

.apply()方法通常更具扩展性,因为您只需转发最初执行的arguments。这样,如果原始函数更改了签名,那么您实际上并不在乎,也不需要更改任何内容。假设现在是setTime(hours, minutes)-转发到原始版本仍然有效。如果您使用.call(),则需要做更多的工作-您需要去更改传递的参数,并且需要将整个替代项修改为类似的

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
}

尽管您可以使用spread syntax

来解决此问题
myAlarm.setTime = function() {//ignore whatever is passed in
  console.log("overriden method!");
  return oldSetTime.call(this, ...arguments); //spread the arguments
}

在这种情况下,.apply(this, arguments).call(this, ...arguments)的结果都相同,但需要进行少量的计划。

Proxy

您可以设置一个代理来侦听并可能修改呼叫,而不是修改对象。在某些情况下,或者只是您所需要的,这可能是一个过大的杀伤力。这是一个覆盖所有方法调用的示例实现

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());

但是,必须小心。如您所见,对setEnabled的调用不会产生日志。这是因为它不通过代理-setTime返回原始对象而不是代理。我将其留给我们展示一个问题。有时,全部覆盖功能太强大了。在这种情况下,如果您想获取myOverridenAlarm.time,就会出现问题,例如,它仍将通过处理程序并将其视为方法。您可以修改处理程序以检查方法,甚至可以检查结果是否为相同的对象(流体接口)并将其包装在代理中,或者适当地返回当前代理,但这样做有点麻烦。这也取决于您的用例。

更简单的方法是通过代理覆盖单个方法。与使用.bind.call.apply的概念非常相似,但在某些方面更可重用。

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());

这是一个更轻量的版本,因为您不会覆盖当前和将来的所有方法,因此它更易于管理。而且,它是可重用的-您只需添加myAlarm.setEnabled = new Proxy(myAlarm.setEnabled, singleMethodHandler);,您将在其中获得相同的功能。因此,如果您只需要有选择地重写具有相同功能的方法(在本例中为日志记录),那么这很容易做到。但是,这确实意味着要更改对象。

如果您想避免更改 instance ,并且希望在所有实例上应用相同的内容,则可以更改对象的 prototype ,以便对该方法将使用代理版本:

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());