简体   繁体   English

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

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

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? 是否可以覆盖Function对象的行为,以便可以在每次调用函数之前注入行为,然后照常进行? 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? 具体来说,(尽管总的想法本身很有趣)我是否可以在不必每次都插入console.log语句的情况下将每个函数调用登录到控制台? And then the normal behavior goes on? 然后正常行为继续吗?

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. 但是,如果它能正常工作,那么在运行的代码上获得1000米的视野似乎是一个不错的解决方案。 And I suspect that the answer will show me something deeper about javascript. 而且我怀疑答案将向我展示有关javascript的更深入的信息。

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.log executes 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. 要点是,我不确定这是否可能。

Intercepting function calls 拦截函数调用

Many here have tried to override .call. 这里的许多人都试图覆盖.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. 我正在回答这个古老的问题,因为它是在我的工作场所提出的,本文将作为参考。

There are only two function-call related functions available for us to modify: .call and .apply. 我们只能修改两个与函数调用相关的函数:.call和.apply。 I will demonstrate a successful override of both. 我将展示两种方法的成功替代。

TL;DR: What OP is asking is not possible. TL; DR:不可能问OP。 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. 答案中的某些成功报告是由于控制台在评估之前就在内部调用了.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. 各种浏览器有各种控制台工具,呼叫.CALL自己有很多 ,其中包括一次对于每个输入,这可能会混淆在当下的用户。 Another mistake is to just console.log arguments, which goes through the console api for stringification, which in turn cause an infinite loop. 另一个错误是只是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. 幸运的是,调用括号不能从JavaScript截获。 But even if .call would intercept the parenthesis operator on function objects, how would we call the original without causing an infinite loop? 但是,即使.call会拦截函数对象上的括号运算符,我们如何在不引起无限循环的情况下调用原始运算符?

The only thing overriding .call/.apply does is to intercept explicit calls to those prototype functions. 覆盖.call / .apply的唯一作用是拦截对这些原型函数的显式调用。 If the console is used with that hack in place, there will be lots and lots of spam. 如果将控制台与适当的hack一起使用,将会有很多垃圾邮件。 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). 此外,如果使用它,还必须非常小心,因为使用控制台API会迅速导致无限循环(如果控制台提供非字符串,console.log将在内部使用.call)。

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 我怀疑只有对“调用”本身的实际调用正在处理中

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

You can override Function.prototype.call , just make sure to only apply functions 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]));
};

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. 
// 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM