如何在运行时获取JavaScript函数调用/跟踪

41

当我在运行时与我的基于AJAX的应用程序交互时,我希望控制台能够输出所有它正在调用的函数。(因此不需要堆栈跟踪、断点或者性能分析等)

例如,假设我在页面上按了一个按钮。我希望看到它返回的所有函数:

所以当我按下按钮时,我想在控制台中看到类似以下内容:

1. button1Clicked();
2.     calculating();
3.          printingResults();

这基本上意味着button1Clicked()调用了calculating(),然后调用了printingResults()。

有什么实用工具、插件、浏览器或者语言内部的方法可以实现吗?顺便说一下,我在使用Google Chrome。

p.s. 我不想逐个函数添加"console.log("inside function X")",因为那太麻烦了。

p.p.s. 如果能看到传递给函数的参数,那就更好了,但或许这有些过分。:>


9
你可以在一个地方使用 console.trace() 而不是在多个地方使用 console.log,这样堆栈信息就会出现在开发者工具中。这个解决方案可行吗? - vcsjones
9个回答

40

我想不出一种很好的方法来全局拦截所有函数调用并插入日志(尽管在下面的更新部分中有一个不错的解决方法)。

相反,怎么样只向你关心的某个命名空间中的函数添加日志呢?您可以使用以下设置代码完成此操作:

var functionLogger = {};

functionLogger.log = true;//Set this to false to disable logging 

/**
 * Gets a function that when called will log information about itself if logging is turned on.
 *
 * @param func The function to add logging to.
 * @param name The name of the function.
 *
 * @return A function that will perform logging and then call the function. 
 */
functionLogger.getLoggableFunction = function(func, name) {
    return function() {
        if (functionLogger.log) {
            var logText = name + '(';

            for (var i = 0; i < arguments.length; i++) {
                if (i > 0) {
                    logText += ', ';
                }
                logText += arguments[i];
            }
            logText += ');';

            console.log(logText);
        }

        return func.apply(this, arguments);
    }
};

/**
 * After this is called, all direct children of the provided namespace object that are 
 * functions will log their name as well as the values of the parameters passed in.
 *
 * @param namespaceObject The object whose child functions you'd like to add logging to.
 */
functionLogger.addLoggingToNamespace = function(namespaceObject){
    for(var name in namespaceObject){
        var potentialFunction = namespaceObject[name];

        if(Object.prototype.toString.call(potentialFunction) === '[object Function]'){
            namespaceObject[name] = functionLogger.getLoggableFunction(potentialFunction, name);
        }
    }
};

然后,对于您想要添加日志记录的任何命名空间对象,只需调用以下代码:

functionLogger.addLoggingToNamespace(yourNamespaceObject);

这里有一个 fiddle,可以看到它的执行情况。

更新
请注意,您可以调用 functionLogger.addLoggingToNamespace(window); 在调用时将日志添加到所有全局函数。此外,如果您真的想要,可以遍历树来查找任何函数并相应地对其进行更新。此方法的唯一缺点是它仅适用于在调用时已经存在的函数。因此,它仍然不是最好的解决方案,但比手动添加日志语句要简单得多 :)


2
@foreyez:没问题,很抱歉我没有一个好的解决方案。同意,我也多次想过这会很不错 :) - Briguy37
发现了一个 bug,在 "func.apply(this, arguments);" 这里改为写成 "return func.apply(this, arguments);",否则它将不会返回任何内容。是的,在对象命名空间上做这件事更好。 - Shai UI
在函数的上下文中,“this”是什么?“this”会变成“functionLogger”,还是保留调用时的任何对象? - jimbobmcgee
@jimbobmcgee 它应该保留正常的上下文。 - Briguy37
如果不涉及类构造函数,那将是很棒的,但是出现了“TypeError: Class constructors cannot be invoked without 'new'”错误。 - Nikita Fuchs
显示剩余6条评论

17

这被称为分析,Chrome和Firebug已经内置此功能。在Chrome开发者工具中,转到“Profiles”选项卡并单击记录(圆形)按钮。执行您的AJAX请求,并在响应后再次单击记录按钮停止。分析的结果将显示在右侧窗格中。

请注意,这将给您提供所有内容,因此,如果您使用的是jQuery之类的库,则绝大多数函数调用对您来说都是无用的。我尝试过几次,但我发现做console.log('inside <method>') 这件事更有帮助。


3
我不需要一个分析器,只是想在控制台中显示函数调用。也许我输入"turnOnTrace()"后可以实现这个功能... 另外,我不想追踪jquery调用。所以它应该给我排除库的选项。最后,分析标签页没有显示我编写的内部函数,所以它完全没用。也许有一种方法可以自动记录每个函数中的console.log。 - Shai UI

5

我刚刚发现你可以使用 console.trace() 来实现这个功能。


4

在Briguy37的解决方案基础上,我写了一个接受调用每个方法之前的函数的解决方案。它还适用于ECMAScript 6类,其中方法不会被for...in枚举。我正在使用它来修改对象原型,以向我的对象的所有新实例添加日志记录。

function inject(obj, beforeFn) {
    for (let propName of Object.getOwnPropertyNames(obj)) {
        let prop = obj[propName];
        if (Object.prototype.toString.call(prop) === '[object Function]') {
            obj[propName] = (function(fnName) {
                return function() {
                    beforeFn.call(this, fnName, arguments);
                    return prop.apply(this, arguments);
                }
            })(propName);
        }
    }
}

function logFnCall(name, args) {
    let s = name + '(';
    for (let i = 0; i < args.length; i++) {
        if (i > 0)
            s += ', ';
        s += String(args[i]);
    }
    s += ')';
    console.log(s);
}

inject(Foo.prototype, logFnCall);

不幸的是,这在类中不起作用:TypeError: Class constructors cannot be invoked without 'new' - Nikita Fuchs

4
让我提出第三种解决方案:全知调试器
请注意,所有其他答案都提供了两种解决方案:
1. 在运行时手动修补JS函数并将其记录到控制台 - 是的,它可以完成工作,但一旦您的项目增长到一定规模,它就会变得无用。除非您不断花时间来开发此功能,否则它不会给您足够的可控性。 2. Jeff建议使用分析器进行调试 - 不是很有帮助,因为分析器视图(至少目前)旨在帮助您分析性能,而不是呼叫图;除非您花费大量时间训练自己适应反生产力的用户界面,否则无法正常工作。
这就是为什么我编写了Dbux——一个VSCode扩展程序,提供具有动态执行分析工具、代码注释和完整的动态调用图可视化工具的全知调试器,旨在帮助开发人员进行程序理解和调试。
一些例子- fibonacci(6)调用图:

在一个屏幕上同时使用代码和其他Dbux工具的调用图:

链接:


3
请尝试使用diyism_trace_for_javascript.htm: https://code.google.com/p/diyism-trace/downloads/list
eval('window.c=function(){3+5;}');
declare_ticks_for(window);

function a(k, c) {
  return k + 2;
}

function b() {
  4 + 3;
  a(3, {'a':'c','b':'d'});
  c();
  return 5 + 4;
}

b();

在Chrome或Firefox的控制台选项卡中查看日志


3

1
你可以通过putout代码转换器来追踪函数调用。 插件将如下所示:
const {template, types, operator} = require('putout');
const {replaceWith} = operator;
const {BlockStatement} = types;

// create nodes
const buildLog = template(`console.log('TYPE' + ' ' + 'NAME')`);
const buildLogEnter = template(`console.log('enter' + ' ' + 'NAME' + '(' + JSON.stringify(Array.from(arguments)) + ')')`);
const buildLogException = template(`console.log('TYPE' + ' ' + 'NAME' + ': ' + trace$error.message); throw trace$error`);
const buildTryCatch = template(`try {
        BLOCK;
    } catch(trace$error) {
        CATCH;
    } finally {
        FINALLY;
    }
`);

const JSON = 'JSON';

// nodes we are searching for
module.exports.include = () => [
    'Function',
];

module.exports.fix = (path) => {
    const name = getName(path);
    
    // create 3 types of events
    const enterLog = buildLogEnter({
        NAME: name,
        JSON,
    });
    const exitLog = buildLogEvent(name, 'exit');
    const errorLog = buildLogExceptionEvent(name);
    
    // move function body into try-catch
    const bodyPath = path.get('body');
    replaceWith(bodyPath, BlockStatement([buildTryCatch({
        BLOCK: path.node.body.body,
        CATCH: errorLog,
        FINALLY: exitLog,
    })]));
    
    // add into the beginning of function "console.log" with "enter" event
    bodyPath.node.body.unshift(enterLog);
};


// get name of a function
function getName(path) {
    if (path.isClassMethod())
        return path.node.key.name;
    
    if (path.isFunctionDeclaration())
        return path.node.id.name;
    
    const {line} = path.node.loc.start;
    return `<anonymous:${line}>`;
}

// build logger
function buildLogEvent(name, type) {    
    return buildLog({
        NAME: name,
        TYPE: type,
    });
}

// build logger that throws
function buildLogExceptionEvent(name) {    
    return buildLogException({
        NAME: name,
        TYPE: 'error',
    });
}

假设这是您想要跟踪的代码:

const processFile = (a) => a;
process([]);

function process(runners) {
    const files = getFiles(runners);
    const linted = lintFiles(files);
    
    return linted;
}

function getFiles(runners) {
    const files = [];
    
    for (const run of runners) {
        files.push(...run());
    }
    
    return files;
}

function lintFiles(files) {
    const linted = [];
    
    for (const file of files) {
        linted.push(processFile(file));
    }
   
    return linted;
}

这是一个完整的图片:

enter image description here

如果您将处理过的源代码保存为trace.js并在Node中运行它,您将会得到:
> node trace.js
enter process([[]])
enter getFiles([[]])
exit getFiles
enter lintFiles([[]])
exit lintFiles
exit process

与跟踪函数相关的发布问题


打包成 https://github.com/coderaiser/estrace。 - milahu

0

我使用了@Briguy37的解决方案并进行了改进。在我的情况下,我不想追踪一些库中的函数,因此我添加了一些代码来排除它们。以下是使用方法:

  • 首先,包含您不想追踪的函数的定义;
  • 使用excludeLoggingToNamespace列出到目前为止定义的函数并将其排除;
  • 包含您想要跟踪的函数的定义;
  • 调用addLoggingToNamespace以向在上一步中定义的函数添加记录功能。

例子:

<script src="js/someLibrary.js"></script>
<script>
    functionLogger.excludeLoggingToNamespace(window);
</script>
<script src="js/codeIWantToTraceHere.js"></script>
<script>
    functionLogger.addLoggingToNamespace(window);
</script>

这是我添加到@Briguy37解决方案中的代码:

var excludedFunctions = {};

        functionLogger.excludeLoggingToNamespace = function(namespaceObject){
            for(var name in namespaceObject){
                var potentialFunction = namespaceObject[name];

                if(Object.prototype.toString.call(potentialFunction) === '[object Function]') {
                    excludedFunctions[name] = name;
                }
            }
        }; 

我不得不修改@Briguy37的addLoggingToNamespace方法,以考虑excludedFunctions哈希表:

functionLogger.addLoggingToNamespace = function(namespaceObject){
    for(var name in namespaceObject){
        var potentialFunction = namespaceObject[name];

        if(Object.prototype.toString.call(potentialFunction) === '[object Function]' && 
           !excludedFunctions[name]) {
            namespaceObject[name] = functionLogger.getLoggableFunction(potentialFunction, name);
        }
    }
};    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接