Если вы пытаетесь переопределить функцию, но теряете контекст 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());
Итак, наивный способ не работает. Есть несколько способов потерять контекст.
Когда вы связываете функцию, вы фактически создаете новую, в которой контекст 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());
Оба очень похожи. В обоих случаях вы будете выполнять функцию и указывать значение контекста 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)
становится идентичным, но требует небольшого планирования заранее.
Вместо изменения объекта вы можете установить прокси, который перехватывает, а возможно изменяет вызовы. Это может быть излишним в некоторых случаях или просто то, что вам нужно. Вот пример реализации, которая переопределяет все вызовы методов
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());