Оперативная замена функции - PullRequest
0 голосов
/ 14 января 2019

У меня есть некоторый код, в котором я хочу динамически заменить функцию, и я подумал об использовании идиомы, используемой в прерываниях ms-dos (исходящих из C ++ / asm background для 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 голосов
/ 14 января 2019

Если вы пытаетесь переопределить функцию, но теряете контекст 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#apply или Function#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
}

Хотя вы можете обойти это, используя синтаксис распространения

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); и получить ту же функциональность. Так что если вам нужно только выборочно переопределить методы с одинаковыми функциями (в данном случае ведение журнала), то это легко сделать. Однако это означает изменение объекта.

Если вы хотите избежать изменения экземпляра и предпочитаете применять одну и ту же вещь ко всем экземплярам, ​​тогда вы можете изменить прототип объекта, чтобы любой вызов метода будет использовать прокси версию:

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());
0 голосов
/ 14 января 2019

Вы можете использовать Function#bind для привязки this к новой функции.

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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...