我可以重写Javascript函数对象来logging所有函数调用?
我可以重写Function对象的行为,以便我可以在每个函数调用之前注入行为,然后继续正常进行? 具体来说(尽pipe总体思路本身就很有趣),我可以在每次函数调用时都login到控制台,而不必在任何地方插入console.log语句。 然后正常的行为继续?
我确实认识到这可能会有重大的性能问题; 即使在我的开发环境中,我也不打算进行这种运行。 但是,如果它能够正常运行,那么在运行代码上获得1000米的视图就是一个很好的解决scheme。 我怀疑,答案会让我更深入地了解JavaScript。
明显的答案如下所示:
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
,…
点是,我不知道这是可能的。
拦截函数调用
很多这里都试图覆盖.call。 有的失败了,有的成功了。 我正在回答这个老问题,因为这个问题已经在我的工作场所提出来了,这个post被用作参考。
只有两个函数调用相关的函数可供我们修改:.call和.apply。 我将展示两者的成功覆盖。
TL; DR:OP所要求的是不可能的。 答案中的一些成功报告是由于控制台在评估之前在内部调用,而不是因为我们想拦截的调用。
覆盖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!
这不是从控制台运行是非常重要的。 各种浏览器都有各种各样的控制台工具,可以调用自己很多 ,包括每次input一次,这可能会混淆用户。 另一个错误是仅仅通过console.log参数,通过控制台api进行string化,这又导致了无限循环。
重写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的唯一function就是拦截对这些原型函数的显式调用。 如果控制台使用了这个黑客,将会有大量的垃圾邮件。 如果使用控制台API,则必须非常小心,因为使用控制台API可能会快速导致无限循环(如果给出非串行控制台,console.log将在内部使用.call)。
我得到了一些结果,并没有页面崩溃与以下内容:
(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中进行testing。
这当然不是logging所有的函数调用,但它正在logging一些! 我怀疑只有实际的呼叫“自己”正在处理
只是一个快速的testing,但它似乎为我工作。 这样做可能没有用处,但是我基本上在恢复原型的同时还原原型,然后在退出之前“解除恢复”。
这个例子简单地logging了所有的函数调用 – 虽然可能有一些致命的缺陷,我还没有发现; 在喝咖啡rest时这样做
履行
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中input的内容,以便testing上述function:
/* 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])); };
我发现使用自动过程来testing文件是最简单的。 我build立了这个小工具,使自己更容易。 也许别人会觉得有用。 它基本上是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. //