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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5226550/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 16:15:26  来源:igfitidea点击:

Can I override the Javascript Function object to log all function calls?

javascript

提问by Matthew Nichols

Can I override the behavior of the Function object so that I can inject behavior prior t every function call, and then carry on as normal? Specifically, (though the general idea is intriguing in itself) can I log to the console every function call without having to insert console.log statements everywhere? And then the normal behavior goes on?

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

I do recognize that this will likely have significant performance problems; I have no intention of having this run typically, even in my development environment. But if it works it seems an elegant solution to get a 1000 meter view on the running code. And I suspect that the answer will show me something deeper about javascript.

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

采纳答案by etlovett

The obvious answer is something like the following:

显而易见的答案如下:

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

But this actually immediately enters an infinite loop, because the very act of calling console.logexecutes a function call, which calls console.log, which executes a function call, which calls console.log, which...

但其实这立即进入一个无限循环,因为这个行为调用的console.log执行函数调用,调用console.log,它执行的函数调用,调用console.log,这...

Point being, I'm not sure this is possible.

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

回答by Kenny

Intercepting function calls

拦截函数调用

Many here have tried to override .call. Some have failed, some have succeeded. I'm responding to this old question, as it has been brought up at my workplace, with this post being used as reference.

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

There are only two function-call related functions available for us to modify: .call and .apply. I will demonstrate a successful override of both.

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

TL;DR: What OP is asking is not possible. Some of the success-reports in the answers are due to the console calling .call internally right before evaluation, not because of the call we want to intercept.

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

Overriding Function.prototype.call

覆盖 Function.prototype.call

This appears to be the first idea people come up with. Some have been more successful than others, but here is an implementation that works:

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

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

This successfully intercepts Function.prototype.call

这成功拦截了 Function.prototype.call

Lets take it for a spin, shall we?

让我们试一试,好吗?

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

It is important that this is not run from a console. The various browsers have all sorts of console tools that call .call themselves a lot, including once for every input, which might confuse a user in the moment. Another mistake is to just console.log arguments, which goes through the console api for stringification, which in turn cause an infinite loop.

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

Overriding Function.prototype.apply as well

也覆盖 Function.prototype.apply

Well, what about apply then? They're the only magic calling functions we have, so lets try that as well. Here goes a version that catches both:

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

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

... And lets try it out!

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

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

As you can see, the calling parenthesis go unnoticed.

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

Conclusion

结论

Fortunately, calling parenthesis cannot be intercepted from JavaScript. But even if .call would intercept the parenthesis operator on function objects, how would we call the original without causing an infinite loop?

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

The only thing overriding .call/.apply does is to intercept explicit calls to those prototype functions. If the console is used with that hack in place, there will be lots and lots of spam. One must furthermore be very careful if it is used, as using the console API can quickly cause an infinite loop (console.log will use .call internally if one gives it an non-string).

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

回答by HBP

I am getting SOME results and no page crashes with the following :

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

(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]));
  });
}) ();

Only tested in Chrome version 9.xxx.

仅在 Chrome 9.xxx 版中测试。

It is certainly not logging all function calls, but it is logging some! I suspect only actual calls to 'call' intself are being processed

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

回答by pentaphobe

Only a quick test, but it seems to work for me. It may not be useful this way, but I'm basically restoring the prototype whilst in my replacement's body and then "unrestoring" it before exiting.

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

This example simply logs all function calls - though there may be some fatal flaw I've yet to detect; doing this over a coffee break

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

implementation

执行

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

usage

用法

I've had some issues including calls to this in one big paste, so here's what I was typing into the REPL in order to test the above functions:

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

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

回答by colllin

You can override Function.prototype.call, just make sure to only applyfunctions within your override.

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

回答by user2171796

I found it easiest to instrument the file, using an automatic process. I built this little tool to make it easier for myself. Perhaps somebody else will find it useful. It's basically awk, but easier for a Javascript programmer to use.

我发现使用自动过程检测文件最容易。我构建了这个小工具是为了让自己更容易。也许其他人会发现它很有用。它基本上是 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. 
//