【问题标题】:What are ways of intercepting function calls or changing a function's behavior?拦截函数调用或改变函数行为的方法有哪些?
【发布时间】:2021-10-20 06:01:59
【问题描述】:

我想在每次调用对象中的某些函数并完成执行时执行一些代码。

对象:

{
    doA() {
        // Does A
    },
    doB() {
        // Does B
    }
}

是否有可能扩展它,改变这些功能,以便他们做他们所做的事情,然后做其他事情?就像是监听那些功能完成的事件?

{
    doA() {
        // Does A
        // Do something else at end
    },
    doB() {
        // Does B
        // Do something else at end
    }
}

也许这可以使用代理https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

尝试使用代理:

const ob = {
    doA() {
        console.log('a');
    },
    doB() {
        console.log('b');
    }
};

const ob2 = new Proxy(ob, {
  apply: function (target, key, value) {
    console.log('c');
  },
});

ob2.doA();

【问题讨论】:

  • 是的,代理可能能够做到这一点 - 你试过了吗?
  • 或任何其他类型的包装器,只是另一个具有相同方法的对象,例如充当代理。
  • VTR:linked question 中的问题和答案都不符合这个用例。
  • @ScottSauyet 什么用例?未指定用例。 OP 给出了通用伪代码。
  • @Iwrestledabearonce:只是包装对象方法的概念。链接的问题是关于扩展内置引用类型。虽然它们在使用代理的可能性上有一些重叠,但这两个问题似乎并不适合彼此。

标签: javascript methods interceptor control-flow proxy-pattern


【解决方案1】:

对于 JavaScript 应用程序,有时需要拦截和/或修改 控制流 一个不拥有或由于其他原因不允许触摸的功能。

对于这种情况,除了通过包装其原始实现来保留和更改此类逻辑之外别无他法。这种能力不是 JavaScript 独有的。通过反射Self-实现元编程的编程语言已有相当长的历史。修改

当然,可以/应该为所有可能想到的修饰符用例提供防弹但方便的抽象。

由于 JavaScript 已经实现了 Function.prototype.bind,它已经带有某种微小的修改能力,我个人不介意 JavaScript 是否有朝一日正式提供定制和标准化的方便 方法- ... Function.prototype[before|around|after|afterThrowing|afterFinally] 的修饰符工具集。

// begin :: closed code
const obj = {
  valueOf() {
    return { foo: this.foo, bar: this.bar };
  },
  toString(link = '-') {
    return [this.foo, this.bar].join(link);
  },
  foo: 'Foo',
  bar: 'Bar',
  baz: 'BAAAZZ'
};
// end :: closed code

console.log(
  'obj.valueOf() ...',
  obj.valueOf()
);
console.log(
  'obj.toString() ...',
  obj.toString()
);


enableMethodModifierPrototypes();


function concatBazAdditionally(proceed, handler, [ link ]) {
  const result = proceed.call(this, link);
  return `${ result }${ link }${ this.baz }`;
}
obj.toString = obj.toString.around(concatBazAdditionally, obj);
// obj.toString = aroundModifier(obj.toString, concatBazAdditionally, obj)

console.log(
  '`around` modified ... obj.toString("--") ...',
  obj.toString("--")
);


function logWithResult(result, args) {
  console.log({ modifyerLog: { result, args, target: this.valueOf() } });
}
obj.toString = obj.toString.after(logWithResult, obj);
// obj.toString = afterModifier(obj.toString, logWithResult, obj)

console.log(
  '`around` and `after` modified ... obj.toString("##") ...',
  obj.toString("##")
);


function logAheadOfInvocation(args) {
  console.log({ stats: { args, target: this } });
}
obj.valueOf = obj.valueOf.before(logAheadOfInvocation, obj);
// obj.valueOf = beforeModifier(obj.valueOf, logAheadOfInvocation, obj)

console.log(
  '`before` modified ... obj.valueOf() ...',
  obj.valueOf()
);


restoreDefaultFunctionPrototype();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
  function isFunction(value) {
    return (
      typeof value === 'function' &&
      typeof value.call === 'function' &&
      typeof value.apply === 'function'
    );
  }

  function getSanitizedTarget(value) {
    return value ?? null;
  }

  function around(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function aroundType(...args) {
        const context = getSanitizedTarget(this) ?? target;

        return handler.call(context, proceed, handler, args);
      }
    ) || proceed;
  }
  around.toString = () => 'around() { [native code] }';

  function before(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function beforeType(...args) {
        const context = getSanitizedTarget(this) ?? target;

        handler.call(context, [...args]);

        return proceed.apply(context, args);
      }
    ) || proceed;
  }
  before.toString = () => 'before() { [native code] }';

  function after(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function afterReturningType(...args) {
        const context = getSanitizedTarget(this) ?? target;
        const result = proceed.apply(context, args);

        handler.call(context, result, args);

        return result;
      }
    ) || proceed;
  }
  after.toString = () => 'after() { [native code] }';

  function aroundModifier(proceed, handler, target) {
    return around.call(proceed, handler, target);
  }
  function beforeModifier(proceed, handler, target) {
    return before.call(proceed, handler, target);
  }
  function afterModifier(proceed, handler, target) {
    return after.call(proceed, handler, target);
  }

  const { prototype: fctPrototype } = Function;

  const methodIndex = {
    around,
    before,
    after/*Returning*/,
    // afterThrowing,
    // afterFinally,
  };
  const methodNameList = Reflect.ownKeys(methodIndex);

  function restoreDefaultFunctionPrototype() {
    methodNameList.forEach(methodName =>
      Reflect.deleteProperty(fctPrototype, methodName),
    );
  }
  function enableMethodModifierPrototypes() {
    methodNameList.forEach(methodName =>
      Reflect.defineProperty(fctPrototype, methodName, {
        configurable: true,
        writable: true,
        value: methodIndex[methodName],
      }),
    );
  }
</script>

<!--
<script src="https://closure-compiler.appspot.com/code/jscd16735554a0120b563ae21e9375a849d/default.js"></script>
<script>
  const {

    disablePrototypes: restoreDefaultFunctionPrototype,
    enablePrototypes: enableMethodModifierPrototypes,
    beforeModifier,
    aroundModifier,
    afterModifier,

  } = modifiers;
</script>
//-->

下一个提供的示例代码使用上述测试对象及其测试用例,但实现/提供了基于代理的解决方案。从需要如何调整测试用例可以看出,基于method-modifier的干净实现的直接方法修改允许更灵活地处理不同的用例,而基于代理的方法仅限于每个拦截的方法调用一个处理函数...

// begin :: closed code
const obj = {
  valueOf() {
    return { foo: this.foo, bar: this.bar };
  },
  toString(link = '-') {
    return [this.foo, this.bar].join(link);
  },
  sayHi() {
    console.log('Hi');
  },
  foo: 'Foo',
  bar: 'Bar',
  baz: 'BAAAZZ'
};
// end :: closed code

console.log(
  'non proxy call ... obj.valueOf() ...',
  obj.valueOf()
);
console.log(
  'non proxy call ... obj.toString() ...',
  obj.toString()
);


function toStringInterceptor(...args) {
  const { proceed, target } = this;
  const [ link ] = args;

  // retrieve the original return value.
  let result = proceed.call(target, link);

  // modify the return value while
  // intercepting the original method call.
  result = `${ result }${ link }${ target.baz }`;

  // log before ...
  console.log({ toStringInterceptorLog: { result, args, target: target.valueOf() } });

  // ... returning the
  // modified value.
  return result;
}

function valueOfInterceptor(...args) {
  const { proceed, target } = this;

  // log before returning ...
  console.log({ valueOfInterceptorLog: { proceed, args, target } });

  // ... and save/keep the
  // original return value.
  return proceed.call(target);
}

function handleTrappedGet(target, key) {
  const interceptors = {
    toString: toStringInterceptor,
    valueOf: valueOfInterceptor,
  }
  const value = target[key];

  return (typeof value === 'function') && (

    interceptors[key]
      ? interceptors[key].bind({ proceed: value, target })
      : value.bind(target)

  ) || value;
}
const objProxy = new Proxy(obj, { get: handleTrappedGet });

console.log('\n+++ proxy `get` handling +++\n\n');

const { foo, bar, baz } = objProxy;
console.log(
  'non method `get` handling ...',
  { foo, bar, baz }
);
console.log('\nproxy call ... objProxy.sayHi() ... but not intercepted ...');
objProxy.sayHi();

console.log('\nintercepted proxy calls ...');
console.log(
  'objProxy.toString("--") ...',
  objProxy.toString("--")
);
console.log(
  'objProxy.valueOf() ...',
  objProxy.valueOf()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

【讨论】:

  • 虽然这里有很多好东西,但可能值得研究这与 OP 的问题有多大关系。如果答案是“不多”,那么这可能更适合作为开发人员的文章发布在某处而不是问答网站。
  • @ScottSauyet ...我喜欢以最不痛苦的方式解决批评的高度先进的技能。我知道,有些人认为上述答案太多。但对我来说它总是值得的。并非每个提问者,尽管没有问过最好的 SO 符合方式或由于声誉低下,自动无法(能力)理解更深入的答案。而且,答案并不总是需要单独针对 OP,因为还有其他读者对主题和/或问题感兴趣。
  • 嗯,我的意思是“调查”。我们没有人回应真正探索 OP 以找到真正的要求。我尝试在@Iwrestledabearonce. 的极简主义和您更彻底的治疗之间取得平衡。但目前还不清楚这是否太多了。如果所有 OP 正在寻找的是在完成主体后做一个console .log,那么有一些简单的解决方案。但它也可以去你做的地方,甚至更远。无论如何,不​​管这里的结果如何,我都希望看到这篇文章以博客文章的形式发布在 medium、dev.to 或类似网站上。
【解决方案2】:

您当然可以使用代理来做到这一点。但是您也可以编写自己的通用函数装饰器来执行此操作。

基本的装饰器可能是这样工作的:

const wrap = (wrapper) => (fn) => (...args) => {
  (wrapper .before || (() => {})) (...args)
  const res = fn (...args)
  const newRes = (wrapper .after || (() => {})) (res, ...args)
  return newRes === undefined ? res : newRes
}

const plus = (a, b) => a + b

const plusPlus = wrap ({
  before: (...args) => console .log (`Arguments: ${JSON.stringify(args)}`),
  after: (res, ...args) => console .log (`Results: ${JSON.stringify(res)}`)
}) (plus)

console .log (plusPlus (5, 7))

我们提供可选函数在主体之前(具有相同的参数)和主体之后(具有结果以及初始参数)运行,并将我们想要装饰的函数传递给结果函数。生成的函数将调用主函数 before,然后调用 after,如果未提供它们,则跳过它们。

要使用它来包装对象的元素,我们可以编写一个处理所有功能的瘦包装器:

const wrap = (wrapper) => (fn) => (...args) => {
  (wrapper .before || (() => {})) (...args)
  const res = fn (...args)
  const newRes = (wrapper .after || (() => {})) (res, ...args)
  return newRes === undefined ? res : newRes
}

const wrapAll = (wrapper) => (o) => Object .fromEntries (
  Object .entries (o) .map (([k, v]) => [k, typeof v == 'function' ? wrap (wrapper) (v) : v])
)

const o = {
    doA () {
        console .log ('Does A')
    },
    doB () {
        console .log ('Does B')
    }
}

const newO = wrapAll ({
  after: () => console .log ('Does something else at end')
}) (o)

newO .doA ()
newO .doB ()

当然,这可以通过多种方式进行扩展。我们可能希望选择要包装的特定函数属性。我们可能想流利地处理this。我们可能希望before 能够更改传递给主函数的参数。我们可能想给生成的函数起一个有用的名字。等等。但是很难为通用包装器设计签名而不是轻松地完成所有这些事情。

【讨论】:

    【解决方案3】:

    这是一个将函数包装在对象中的简单方法的示例。

    这对于调试很有用,但您不应该在生产环境中这样做,因为您(或稍后查看您的代码的任何人)将很难弄清楚为什么您的方法正在执行原始代码中没有的事情.

    var myObj = {
      helloWorld(){
        console.log('Hello, world!');
      }
    }
    
    
    // get a refernce to the original function
    var f = myObj.helloWorld;
    
    // overwrite the original function
    myObj.helloWorld = function(...args){
      
      // call the original function first
      f.call(this, ...args);
      
      // Then  do other stuff afterwards
      console.log('Goodbye, cruel world..');
    };
    
    
    myObj.helloWorld();

    【讨论】:

    • 当前提供的包装器解决方案没有考虑任何this 上下文和arguments 处理,这对于任何方法调用/调用都至关重要。
    • @PeterSeliger 我添加了对this 和参数的支持,尽管这些都不是 OP 的一部分,尽管你的投诉的后半部分显然是错误的。您的吹毛求疵并没有为这个问题增加任何价值。我还是关闭了这个问题。
    • 感谢您的更新(因此赞成)。我的第一条评论不仅仅是为了吹毛求疵。它是关于精确并提前知道经验不足的开发人员会绊倒的下一个绊脚石。可以通过首先提供足够好的/可行的示例代码和解释来提前保存 OP。
    • ...我重新打开了它,因为建议的副本似乎不是提议。如果我们正在尝试编写一个非常通用的方法包装器,那么 Peter 的 cmets 就非常合适,但是从问题中根本不清楚是否是这样。我采用了一种处理参数的中间方法,但将this 放在一边……可能是因为我自己很少使用this
    【解决方案4】:

    使用 Proxy,我们可以定位所有包含函数的 get,然后我们可以检查所获取的是否是函数,如果是,我们创建并返回包装对象函数并调用它的自己的函数。

    从我们的包装函数中调用对象函数后,我们可以执行任何我们想要的操作,然后返回对象函数的返回值。

    const ob = {
      doA(arg1, arg2) {
        console.log(arg1, arg2);
        return 1;
      },
      doB() {
        console.log('b');
      }
    };
    
    const ob2 = new Proxy(ob, {
      get: function(oTarget, sKey) {
        if (typeof oTarget[sKey] !== 'function')    return oTarget[sKey];
    
        return function(...args) {
          const ret = oTarget[sKey].apply(oTarget, args);
    
          console.log("c");
    
          return ret;
        }
      }
    });
    
    console.log(ob2.doA('aaa', 'bbb'));

    如果有改进或其他选项,请添加评论!

    【讨论】:

    • 1/3 ... 当然可以采用这种方法。只需记住,您已经创建了一个陷阱来获取任何属性值。因此,处理程序还需要测试 typeof oTarget[sKey]'function' 类型。否则,将在任何其他但不可调用的类型上使用 call/() 运算符,例如尝试得到obj.foo 中的foo,这可能是之前通过obj.foo = 'Foo' 设置的。在这种情况下,处理程序需要按原样返回oTarget[sKey]
    • 2/3 ... 还需要记住,如果使用正确的函数类型检查实现上述处理程序,则确实会创建并返回另一个函数,每个有效的捕获 get,无论该函数是否会(立即)被调用。人们还应该注意到,上面的实现确实针对对象的任何方法,并且处理它们的方式都是一样的。如果要针对特定​​方法,例如obj.doB,则必须在代理处理程序中提供这样的sKey 特定逻辑。
    • 3/3 ... 最后,由于确实代理了一个对象并且确实对此类对象进行了目标方法调用,因此需要将oTarget[sKey](...arguments) 更改为oTarget[sKey].apply(oTarget, args)
    • 非常感谢@PeterSeliger。我已根据您的建议更新了代码。
    【解决方案5】:

    如果是关于拥有相同的事件侦听器机制,您可以创建一个对象来存储函数并在需要时执行它们

    const emitter =  {
        events: {},    
    
        addListener(event, listener) {
            this.events[event] = this.events[event] || [];
            this.events[event].push(listener);
        },
    
        emit(event, data) {
            if(this.events[event]) {
                this.events[event].forEach(listener => listener(data));
            }
        }
    }
    
    //instead of the config object you could just type the string for the event
    const config = {
        doA: 'doA',
        doB: 'doB'
    }
    
    //store first function for doA
    emitter.addListener(config.doA, (data) => {
        console.log('hardler for Function ' + data + ' executed!');
    });
    
    //store second function for doA
    emitter.addListener(config.doA, () => {
        console.log('Another hardler for Function A executed!');
    });
    
    //store first function for doB
    emitter.addListener(config.doB, (data) => {
        console.log('hardler for Function ' + data + ' executed!');
    });
    
    let obj = {
        doA() {
            let char = 'A';
            console.log('doA executed!');
            //You can pass data to the listener
            emitter.emit(config.doA, char);
        },
    
        doB() {
            let char = 'B';
            console.log('doB executed!');
            emitter.emit(config.doB, char);
        }
    }
    
    obj.doA();
    obj.doB();
    
    //Output:
    //doA executed!
    //hardler for Function A executed!
    //Another hardler for Function A executed!
    //doB executed!
    //hardler for Function B executed!
    

    【讨论】:

    • 这种方法确实依赖于更改方法的原始实现。 OP没有提到,但OP的痛苦可能来自于OP不能触摸/不允许触摸这种封闭代码。
    猜你喜欢
    • 2018-08-25
    • 2011-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-17
    • 1970-01-01
    相关资源
    最近更新 更多