我可以覆盖 Javascript Function 对象来记录所有函数调用吗?

IT技术 javascript
2021-01-19 08:00:45

我可以覆盖 Function 对象的行为,以便我可以在每次函数调用之前注入行为,然后照常进行吗?具体来说,(尽管总体思路本身很有趣)我可以在每次函数调用时登录到控制台,而不必在任何地方插入 console.log 语句吗?然后正常的行为会继续吗?

我确实认识到这可能会产生严重的性能问题;即使在我的开发环境中,我也不打算通常运行此程序。但是,如果它有效,那么在运行代码上获得 1000 米的视图似乎是一个优雅的解决方案。我怀疑这个答案会让我更深入地了解 javascript。

6个回答

显而易见的答案类似于以下内容:

var origCall = Function.prototype.call;
Function.prototype.call = function (thisArg) {
    console.log("calling a function");

    var args = Array.prototype.slice.call(arguments, 1);
    origCall.apply(thisArg, args);
};

但这实际上立即进入了一个无限循环,因为调用的行为console.log本身就是执行一个函数调用,它调用console.log,它执行一个函数调用,它调用console.log...

重点是,我不确定这是可能的。

您只需使用 for 循环对其进行迭代即可。
2021-03-15 08:00:45
为了解决无限循环问题,我想您可以检查是否Function#caller等于Function#call并在这种情况下跳过console.log调用。
2021-03-18 08:00:45
实际上,是 Array.prototype.slice.call(...) 触发了递归,因为它是从 Function.prototype 继承的,您已经在那里覆盖了它。执行“console.log(...);”时不会使用Function.prototype.call(...)
2021-04-11 08:00:45

拦截函数调用

这里的许多人都试图覆盖 .call。有的失败了,有的成功了。我正在回答这个老问题,因为它是在我的工作场所提出的,这篇文章被用作参考。

只有两个函数调用相关的函数可供我们修改:.call 和.apply。我将展示对两者的成功覆盖。

TL;DR:OP 所要求的是不可能的。答案中的一些成功报告是由于控制台在评估之前内部调用 .call ,而不是因为我们要拦截的调用。

覆盖 Function.prototype.call

这似乎是人们提出的第一个想法。有些比其他的更成功,但这里有一个有效的实现:

// Store the original
var origCall = Function.prototype.call;
Function.prototype.call = function () {
    // If console.log is allowed to stringify by itself, it will
    // call .call 9 gajillion times. Therefore, lets do it by ourselves.
    console.log("Calling",
                Function.prototype.toString.apply(this, []),
                "with:",
                Array.prototype.slice.apply(arguments, [1]).toString()
               );

    // A trace, for fun
   console.trace.apply(console, []);

   // The call. Apply is the only way we can pass all arguments, so don't touch that!
   origCall.apply(this, arguments);
};

这成功拦截了 Function.prototype.call

让我们试一试,好吗?

// Some tests
console.log("1"); // Does not show up
console.log.apply(console,["2"]); // Does not show up
console.log.call(console, "3"); // BINGO!

重要的是这不是从控制台运行。各种浏览器有各种控制台工具,呼叫.CALL自己有很多,其中包括一次对于每个输入,这可能会混淆在当下的用户。另一个错误是只使用 console.log 参数,它通过控制台 api 进行字符串化,从而导致无限循环。

也覆盖 Function.prototype.apply

那么,申请呢?它们是我们拥有的唯一神奇的调用函数,所以让我们也尝试一下。这是一个同时捕获两者的版本:

// Store apply and call
var origApply = Function.prototype.apply;
var origCall = Function.prototype.call;

// We need to be able to apply the original functions, so we need
// to restore the apply locally on both, including the apply itself.
origApply.apply = origApply;
origCall.apply = origApply;

// Some utility functions we want to work
Function.prototype.toString.apply = origApply;
Array.prototype.slice.apply = origApply;
console.trace.apply = origApply;

function logCall(t, a) {
    // If console.log is allowed to stringify by itself, it will
    // call .call 9 gajillion times. Therefore, do it ourselves.
    console.log("Calling",
                Function.prototype.toString.apply(t, []),
                "with:",
                Array.prototype.slice.apply(a, [1]).toString()
               );
    console.trace.apply(console, []);
}

Function.prototype.call = function () {
   logCall(this, arguments);
   origCall.apply(this, arguments);
};

Function.prototype.apply = function () {
    logCall(this, arguments);
    origApply.apply(this, arguments);
}

......让我们试试吧!

// Some tests
console.log("1"); // Passes by unseen
console.log.apply(console,["2"]); // Caught
console.log.call(console, "3"); // Caught

如您所见,调用括号没有被注意到。

结论

幸运的是,调用括号无法从 JavaScript 中截获。但即使 .call 会拦截函数对象上的括号运算符,我们如何调用原始函数而不导致无限循环?

覆盖 .call/.apply 的唯一作用是拦截对这些原型函数的显式调用。如果控制台与该黑客一起使用,将会有很多垃圾邮件。此外,如果使用它,必须非常小心,因为使用控制台 API 会迅速导致无限循环(如果给它一个非字符串,console.log 将在内部使用 .call)。

在什么浏览器?我刚刚测试过(粘贴最后一个 blob 并运行“让我们试一试”代码),它按预期工作......?
2021-03-15 08:00:45
在 2021 年。示例不起作用。不在 chrome 控制台中。不是作为节点脚本
2021-03-24 08:00:45
当我尝试这个时,我在行中收到此错误:“JavaScript 运行时错误:Function.prototype.call: 'this' is not a Function object” origCall.apply(this, arguments);
2021-03-30 08:00:45
嗯,问题是这曾经在旧版本的引擎中工作。覆盖调用和应用用于捕获所有函数调用。这已在较新版本的浏览器中得到修复。
2021-04-12 08:00:45
如果方法返回一些东西,被覆盖的调用方法不应该返回那个值,即 return origCall.apply(this,arguments);?
2021-04-12 08:00:45

我得到了一些结果并且没有页面崩溃,如下所示:

(function () {
  var 
    origCall = Function.prototype.call,
    log = document.getElementById ('call_log');  

  // Override call only if call_log element is present    
  log && (Function.prototype.call = function (self) {
    var r = (typeof self === 'string' ? '"' + self + '"' : self) + '.' + this + ' ('; 
    for (var i = 1; i < arguments.length; i++) r += (i > 1 ? ', ' : '') + arguments[i];  
    log.innerHTML += r + ')<br/>';



    this.apply (self, Array.prototype.slice.apply (arguments, [1]));
  });
}) ();

仅在 Chrome 9.xxx 版中测试。

它当然不会记录所有函数调用,但会记录一些!我怀疑只处理对“调用”自身的实际调用

@Johannes 我遇到了一些图书馆的类似问题。'apply' 函数可能会再次调用 'call' 并陷入循环。我决定将控制台日志添加到代码本身是一种更好的方法:github.com/garysweaver/noisify注意:为了将来帮助解决 autolog/etc 中的问题。在github的项目中打开一个问题谢谢你的报告。
2021-03-15 08:00:45
@GaryS.Weaver -- 是的,我看到了 noisify,但我正在寻找 JS 解决方案。无论如何,感谢您回复我。
2021-03-18 08:00:45
谢谢!在格式化和记录到 console.log 的东西中使用了一些代码,称之为autolog.js试试吧,让我知道你的想法!仍然需要大量的工作。
2021-03-24 08:00:45
谢谢,您可能会发现我对stackoverflow.com/questions/6921588 的回答也很有帮助。
2021-03-28 08:00:45
@GaryS.Weaver——你的自动日志只是在 Chrome 中一遍又一遍地打印到我的控制台: [object Arguments].slice () source: at Function.function_prototype_call_override (<anonymous>:76:17) [object HTMLBodyElement].anonymous ((object)[object Object], (object)[object Object],[object Object]) source: at Function.function_prototype_call_override (<anonymous>:76:17)
2021-04-08 08:00:45

只是一个快速测试,但它似乎对我有用。这种方式可能没有用,但我基本上是在替代者的身体中恢复原型,然后在退出之前“恢复”它。

这个例子简单地记录了所有的函数调用——尽管可能有一些我还没有发现的致命缺陷;在喝咖啡休息时这样做

执行

callLog = [];

/* set up an override for the Function call prototype
 * @param func the new function wrapper
 */
function registerOverride(func) {
   oldCall = Function.prototype.call;
   Function.prototype.call = func;
}

/* restore you to your regular programming 
 */
function removeOverride() {
   Function.prototype.call = oldCall;
}

/* a simple example override
 * nb: if you use this from the node.js REPL you'll get a lot of buffer spam
 *     as every keypress is processed through a function
 * Any useful logging would ideally compact these calls
 */
function myCall() { 
   // first restore the normal call functionality
   Function.prototype.call = oldCall;

   // gather the data we wish to log
   var entry = {this:this, name:this.name, args:{}};
   for (var key in arguments) {
     if (arguments.hasOwnProperty(key)) {
      entry.args[key] = arguments[key];
     }
   }
   callLog.push(entry);

   // call the original (I may be doing this part naughtily, not a js guru)
   this(arguments);

   // put our override back in power
   Function.prototype.call = myCall;
}

用法

我遇到了一些问题,包括在一个大粘贴中调用这个,所以这是我在 REPL 中输入的内容,以测试上述功能:

/* example usage
 * (only tested through the node.js REPL)
 */
registerOverride(myCall);
console.log("hello, world!");
removeOverride(myCall);
console.log(callLog);

您可以覆盖Function.prototype.call,只需确保仅apply在您的覆盖范围内起作用。

window.callLog = [];
Function.prototype.call = function() {
    Array.prototype.push.apply(window.callLog, [[this, arguments]]);
    return this.apply(arguments[0], Array.prototype.slice.apply(arguments,[1]));
};