簡體   English   中英

我可以覆蓋Javascript Function對象來記錄所有函數調用嗎?

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

是否可以覆蓋Function對象的行為,以便可以在每次調用函數之前注入行為,然后照常進行? 具體來說,(盡管總的想法本身很有趣)我是否可以在不必每次都插入console.log語句的情況下將每個函數調用登錄到控制台? 然后正常行為繼續嗎?

我確實意識到這可能會帶來嚴重的性能問題; 即使在我的開發環境中,我也無意通常運行該程序。 但是,如果它能正常工作,那么在運行的代碼上獲得1000米的視野似乎是一個不錯的解決方案。 而且我懷疑答案將向我展示有關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。 有些失敗了,有些成功了。 我正在回答這個古老的問題,因為它是在我的工作場所提出的,本文將作為參考。

我們只能修改兩個與函數調用相關的函數:.call和.apply。 我將展示兩種方法的成功替代。

TL; DR:不可能問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

好吧,那申請呢? 它們是我們僅有的魔術調用函數,因此也請嘗試一下。 這里有一個兼具兩者的版本:

// 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的唯一作用是攔截對這些原型函數的顯式調用。 如果將控制台與適當的hack一起使用,將會有很多垃圾郵件。 此外,如果使用它,還必須非常小心,因為使用控制台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中進行了測試。

它當然不是在記錄所有函數調用,而是在記錄一些! 我懷疑只有對“調用”本身的實際調用正在處理中

只是快速測試,但這似乎對我有用。 這樣可能沒有用,但是我基本上是在替換的身體中還原原型,然后在退出之前“還原”它。

這個示例只是記錄所有函數調用-盡管可能還存在一些致命的缺陷,但我還沒有發現。 在喝咖啡休息時間做這個

實作

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

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

我發現使用自動過程最容易檢測文件。 我構建了這個小工具,以使自己更輕松。 也許其他人會發現它有用。 它基本上是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