我能否覆盖Javascript函数对象以记录所有函数调用?

60

我是否可以覆盖Function对象的行为,以便在每个函数调用之前注入行为,然后继续正常执行?具体来说(尽管整个想法本身很有趣),我是否可以在不必在所有位置插入console.log语句的情况下记录到控制台中的每个函数调用? 然后正常的行为继续执行?

我确实认识到这可能会带来显著的性能问题; 我没有意图让它通常运行,即使在我的开发环境中也是如此。 但如果它有效,似乎是一个优雅的解决方案,以获取正在运行的代码的1000米视图。 而且,我怀疑答案将向我展示关于javascript更深层次的东西。


2
我真的希望这是不可能的 :-) - stefan
3
已在此回答:https://dev59.com/Pm445IYBdhLWcg3wGmk6 - Wayne
1
我确实看到了那个...但它并不完全符合我的要求。我想要的是能够附加到所有函数调用的东西,而那个只是命中全局命名空间而不进行深入。如果找不到更好的答案,我可能会将其用作递归执行任务的基础,但我非常想知道是否可以通过调整基本Function对象本身来完成这项工作。 - Matthew Nichols
1
从技术上讲,处理这个问题的方法可能是通过覆盖Function.prototype.call来使用包装器方法。如果您在Chrome的控制台中执行此操作,您将立即看到日志显示出现在您输入时(因为它运行函数来评估您正在键入的内容)。然而,如果您尝试将其放入实际页面中,它会惊人地崩溃。 - Nathan Ostgard
1
@Nathan,如果你在回答中放上一个代码示例,即使它出现错误,我也很可能会将其标记为已接受,因为这正是我正在寻找的深层机制。 - Matthew Nichols
显示剩余2条评论
6个回答

26

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

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……

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


9
为了解决无限循环的问题,我建议你可以检查Function#caller是否等于Function#call,如果是,则跳过console.log调用。 - Mathias Bynens
8
实际上,触发递归的是 Array.prototype.slice.call(...),因为它继承自 Function.prototype,而您已经在那里覆盖了它。当执行“console.log(...);”时,不会使用 Function.prototype.call(...)。 - Dan Phillimore
1
你可以使用for循环进行迭代。 - Benjamin Gruenbaum

19

拦截函数调用

许多人已经尝试覆盖 .call。一些人失败了,一些人成功了。 我在回答这个老问题时进行回应,在我的工作场所提到了这篇文章。

我们只能修改两个与函数调用相关的函数:.call 和 .apply。我将演示对它们的成功覆盖。

简而言之,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 方法
那么 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截获。
结论
幸运的是,JavaScript无法截获调用括号。但即使.call在函数对象上截获了括号操作符,我们又该如何调用原始函数而不会导致无限循环呢?
重载.call/.apply唯一做的事情就是截获对这些原型函数的显式调用。如果在此hack的情况下使用控制台,将会有大量的垃圾信息。如果使用控制台API,必须非常小心,因为console.log将在内部使用.call(如果给它一个非字符串参数)。

当我尝试运行时,出现了这个错误:"JavaScript运行时错误:Function.prototype.call:'this'不是一个函数对象",位于代码行origCall.apply(this, arguments); - Sen Jacob
在哪个浏览器中?我刚刚测试了一下(将最终的 blob 粘贴并运行“让我们试试”代码),它按预期工作了...? - Kenny
2
嗯,问题在于这在旧版本的引擎中曾经是有效的。覆盖 call 和 apply 可以捕获所有函数调用。但这在较新版本的浏览器中已经被修复了。 - Benjamin Gruenbaum
1
如果该方法返回某些内容,那么重写的调用方法不应该返回该值吗?例如:return origCall.apply(this,arguments); - Ayush Goel
在2021年,例如无法工作,不在Chrome控制台中,也不作为节点脚本。 - Eva Cohen

4
我用以下方法得到了一些结果,并且没有页面崩溃:
(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版本中进行过测试。

它肯定不会记录所有的函数调用,但是它确实记录了一些!我怀疑只有对'call'本身的实际调用才会被处理。


1
谢谢!我在某个格式化并记录到 console.log 的东西中使用了一点那段代码,将其称为 autolog.js。试试看,并告诉我你的想法!仍需要大量的工作。 - Gary S. Weaver
1
谢谢,你可能会发现我对https://dev59.com/YWw05IYBdhLWcg3w9mdW的回答也很有帮助。 - HBP
@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) - Hanna
@Johannes 我在使用一些库时遇到了类似的问题。'apply'函数可能会再次调用'call'而导致陷入循环。我决定向代码本身添加控制台日志记录可能是更好的方法:https://github.com/garysweaver/noisify 注意:如果将来在autolog / etc中遇到问题需要帮助,请在Github项目中打开一个[issue](https://github.com/garysweaver/autolog.js/issues)。感谢您的报告。 - Gary S. Weaver
@GaryS.Weaver -- 是的,我看到了noisify,但我正在寻找JS解决方案。无论如何,还是谢谢你回复我。 - Hanna
显示剩余2条评论

3

只是一个快速测试,但在我的机器人身体中恢复原型并在退出之前“取消还原”,似乎对我有效。

这个示例仅记录所有函数调用 - 虽然可能存在我尚未检测到的致命缺陷; 在休息时间完成。

实现

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

2

你可以覆盖 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]));
};

0

我发现使用自动化过程来对文件进行仪表化是最容易的。我建立了这个小工具,使得对我自己来说更容易。也许其他人也会觉得有用。它基本上是awk,但更容易让Javascript程序员使用。

// This tool reads a file and builds a buffer of say ten lines.  
// When a line falls off the end of the buffer, it gets written to the output file. 
// When a line is read from the input file, it gets written to the first line of the buffer. 
// After each occurrence of a line being read from the input file and/or written to the output 
// file, a routine is given control.  The routine has the option of operating on the buffer.  
// It can insert a line before or after a line that is there, based on the lines surrounding. 
// 
// The immediate case is that if I have a set of lines like this: 
// 
//             getNum: function (a, c) {
//                 console.log(`getNum: function (a, c) {`);
//                 console.log(`arguments.callee = ${arguments.callee.toString().substr(0,100)}`);
//                 console.log(`arguments.length = ${arguments.length}`);
//                 for (var i = 0; i < arguments.length; i++) { console.log(`arguments[${i}] = ${arguments[i] ? arguments[i].toString().substr(0,100) : 'falsey'}`); }
//                 var d = b.isStrNum(a) ? (c && b.isString(c) ? RegExp(c) : b.getNumRegx).exec(a) : null;
//                 return d ? d[0] : null
//             },
//             compareNums: function (a, c, d) {
//                 console.log(`arguments.callee = ${arguments.callee.toString().substr(0,100)}`);
// 
// I want to change that to a set of lines like this: 
// 
//             getNum: function (a, c) {
//                 console.log(`getNum: function (a, c) {`);
//                 console.log(`arguments.callee = ${arguments.callee.toString().substr(0,100)}`);
//                 console.log(`arguments.length = ${arguments.length}`);
//                 for (var i = 0; i < arguments.length; i++) { console.log(`arguments[${i}] = ${arguments[i] ? arguments[i].toString().substr(0,100) : 'falsey'}`); }
//                 var d = b.isStrNum(a) ? (c && b.isString(c) ? RegExp(c) : b.getNumRegx).exec(a) : null;
//                 return d ? d[0] : null
//             },
//             compareNums: function (a, c, d) {
//                 console.log(`compareNums: function (a, c, d) {`);
//                 console.log(`arguments.callee = ${arguments.callee.toString().substr(0,100)}`);
// 
// We are trying to figure out how a set of functions work, and I want each function to report 
// its name when we enter it.
// 
// To save time, options and the function that is called on each cycle appear at the beginning 
// of this file.  Ideally, they would be --something options on the command line. 


const readline = require('readline');


//------------------------------------------------------------------------------------------------

// Here are the things that would properly be options on the command line.  Put here for 
// speed of building the tool. 

const frameSize = 10;
const shouldReportFrame = false;

function reportFrame() {
    for (i = frame.length - 1; i >= 0; i--) {
        console.error(`${i}.  ${frame[i]}`);  // Using the error stream because the stdout stream may have been coopted. 
    }
}

function processFrame() {
    // console.log(`********  ${frame[0]}`);
    // if (frame[0].search('console.log(\`arguments.callee = \$\{arguments.callee.toString().substr(0,100)\}\`);') !== -1) {
    // if (frame[0].search('arguments.callee') !== -1) {
    // if (frame[0].search(/console.log\(`arguments.callee = \$\{arguments.callee.toString\(\).substr\(0,100\)\}`\);/) !== -1) {
    var matchArray = frame[0].match(/([ \t]*)console.log\(`arguments.callee = \$\{arguments.callee.toString\(\).substr\(0,100\)\}`\);/);
    if (matchArray) {
        // console.log('********  Matched');
        frame.splice(1, 0, `${matchArray[1]}console.log('${frame[1]}');`);
    }
}

//------------------------------------------------------------------------------------------------


var i;
var frame = [];

const rl = readline.createInterface({
    input: process.stdin
});

rl.on('line', line => {
    if (frame.length > frameSize - 1) {
        for (i = frame.length - 1; i > frameSize - 2; i--) {
            process.stdout.write(`${frame[i]}\n`);
        }
    }
    frame.splice(frameSize - 1, frame.length - frameSize + 1);
    frame.splice(0, 0, line);
    if (shouldReportFrame) reportFrame();
    processFrame();
    // process.stdout.write(`${line}\n`);  // readline gives us the line with the newline stripped off
});

rl.on('close', () => {
    for (i = frame.length - 1; i > -1; i--) {
        process.stdout.write(`${frame[i]}\n`);
    }
});


// Notes
// 
// We are not going to control the writing to the output stream.  In particular, we are not 
// going to listen for drain events.  Nodejs' buffering may get overwhelmed. 
// 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接