Can I override the Javascript Function object to log all function calls?
Solution 1:
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...
Point being, I'm not sure this is possible.
Solution 2:
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.
There are only two function-call related functions available for us to modify: .call and .apply. I will demonstrate a successful override of both.
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.
Overriding 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
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.
Overriding Function.prototype.apply as well
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?
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).
Solution 3:
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.
It is certainly not logging all function calls, but it is logging some! I suspect only actual calls to 'call' intself are being processed
Solution 4:
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:
/* example usage
* (only tested through the node.js REPL)
*/
registerOverride(myCall);
console.log("hello, world!");
removeOverride(myCall);
console.log(callLog);