【问题标题】:Extracting nested function names from a JavaScript function从 JavaScript 函数中提取嵌套函数名称
【发布时间】:2010-10-05 18:26:44
【问题描述】:

给定一个函数,我试图找出其中嵌套函数的名称(只有一层深)。

一个针对toString() 的简单正则表达式一直有效,直到我开始使用其中包含 cmets 的函数。事实证明,一些浏览器存储部分原始源代码,而另一些浏览器则根据编译后的内容重建源代码; toString() 的输出可能包含某些浏览器中的原始代码 cmets。顺便说一下,这是我的发现:

测试对象

function/*post-keyword*/fn/*post-name*/()/*post-parens*/{
    /*inside*/
}

document.write(fn.toString());

结果

浏览器 post-keyword post-name post-parens 里面 ----------- ------------ --------- ----------- -------- - 火狐 不 不 不 不 野生动物园 不 不 不 不 Chrome 否 否 是 是 IE 是 是 是 是 歌剧 是 是 是 是

我正在寻找一种从给定函数中提取嵌套函数名称的跨浏览器方法。该解决方案应该能够从以下函数中提取“fn1”和“fn2”:

function someFn() {
    /**
     * Some comment
     */
     function fn1() {
         alert("/*This is not a comment, it's a string literal*/");
     }

     function // keyword
     fn2 // name
     (x, y) // arguments
     {
         /*
         body
         */
     }

     var f = function () { // anonymous, ignore
     };
}

解决方案不必是纯正则表达式。

更新:您可以假设我们一直在处理有效、正确嵌套的代码,所有字符串文字、cmets 和块都正确终止。这是因为我正在解析一个已经编译为有效函数的函数。

更新 2:如果您想知道这背后的动机:我正在开发一个名为 jsUnity 的新 JavaScript 单元测试框架。您可以使用几种不同的格式编写测试和测试套件。其中之一是函数:

function myTests() {
    function setUp() {
    }

    function tearDown() {
    }

    function testSomething() {
    }

    function testSomethingElse() {
    }
}

由于函数隐藏在闭包中,我无法从函数外部调用它们。因此,我将外部函数转换为字符串,提取函数名称,在底部附加“现在运行给定的内部函数”语句,并将其重新编译为带有新 Function() 的函数。如果测试函数中包含 cmets,则提取函数名称并避免误报会变得很棘手。因此,我正在寻求 SO 社区的帮助...

Update3:我提出了a new solution,它不需要大量的语义摆弄代码。我使用原始源本身来探测一级函数。

【问题讨论】:

  • 查看使用 Function.toString() 而非 Function.toSource() 时的结果。此外,您非常接近词法分析器/解析器领域
  • 哇。抱歉,这是问题中的错字。我正在使用 toString()。我会解决这个问题。
  • 这背后的动机是什么——你为什么需要这样做?
  • ps:您的someFn() 函数中存在一个错误,内部函数2:您正在注释掉左括号;我的解决方案现在产生了所需的结果...
  • @Peter Boughton:我会提到这个问题的动机。

标签: javascript regex parsing function


【解决方案1】:

外观变化和错误修复

正则表达式必须阅读\bfunction\b以避免误报!

如果nested 的计算结果不是true,则块中定义的函数(例如循环体中)将被忽略。

function tokenize(code) {
    var code = code.split(/\\./).join(''),
        regex = /\bfunction\b|\(|\)|\{|\}|\/\*|\*\/|\/\/|"|'|\n|\s+/mg,
        tokens = [],
        pos = 0;

    for(var matches; matches = regex.exec(code); pos = regex.lastIndex) {
        var match = matches[0],
            matchStart = regex.lastIndex - match.length;

        if(pos < matchStart)
            tokens.push(code.substring(pos, matchStart));

        tokens.push(match);
    }

    if(pos < code.length)
        tokens.push(code.substring(pos));

    return tokens;
}

var separators = {
    '/*' : '*/',
    '//' : '\n',
    '"' : '"',
    '\'' : '\''
};

function extractInnerFunctionNames(func, nested) {
    var names = [],
        tokens = tokenize(func.toString()),
        level = 0;

    for(var i = 0; i < tokens.length; ++i) {
        var token = tokens[i];

        switch(token) {
            case '{':
            ++level;
            break;

            case '}':
            --level;
            break;

            case '/*':
            case '//':
            case '"':
            case '\'':
            var sep = separators[token];
            while(++i < tokens.length && tokens[i] !== sep);
            break;

            case 'function':
            if(level === 1 || (nested && level)) {
                while(++i < tokens.length) {
                    token = tokens[i];

                    if(token === '(')
                        break;

                    if(/^\s+$/.test(token))
                        continue;

                    if(token === '/*' || token === '//') {
                        var sep = separators[token];
                        while(++i < tokens.length && tokens[i] !== sep);
                        continue;
                    }

                    names.push(token);
                    break;
                }
            }
            break;
        }
    }

    return names;
}

【讨论】:

  • 感谢克里斯托夫的回答。我会写一些单元测试,看看它是否满足所有场景。我也在发起赏金,看看是否有人能提出更短的解决方案。
  • 刚刚想出了这个替代方案:stackoverflow.com/questions/517411/…
  • 在循环内声明的函数并不是真正的“嵌套”,就像循环内的“var”声明并不是真正的嵌套一样。这些函数在循环之外也是可见的。
【解决方案2】:

学术上正确的处理方法是为由正式语法生成的 Javascript 子集(函数定义)创建词法分析器和解析器(例如,请参阅有关该主题的 this link)。
查看JS/CC,了解 Javascript 解析器生成器。

其他解决方案只是 regex hack,这会导致无法维护/不可读的代码,并且在特定情况下可能会导致隐藏的解析错误。

附带说明一下,我不确定您为什么不以不同的方式指定产品中的单元测试函数列表(函数数组?)。

【讨论】:

  • jsUnity 支持多种格式,包括函数数组。我喜欢闭包语法的一点是它的紧凑性和与 jUnit 测试的相似性。
  • JS/CC 看起来很有趣,似乎是实现我想要的正确途径。
【解决方案3】:

如果您将测试定义为:

var tests = {
    test1: function (){
        console.log( "test 1 ran" );
    },

    test2: function (){
        console.log( "test 2 ran" );
    },

    test3: function (){
        console.log( "test 3 ran" );
    }
};

然后你可以像这样轻松地运行它们:

for( var test in tests ){ 
    tests[test]();
}

这看起来更容易。 您甚至可以通过这种方式在 JSON 中进行测试。

【讨论】:

【解决方案4】:

我喜欢你用jsUnity 做的事情。当我看到我喜欢的东西(并且有足够的空闲时间;))时,我会尝试以更适合我需求的方式重新实现它(也称为“这里没有发明”综合症)。

我努力的结果在this article中描述,代码可以在here找到。

随意撕掉你喜欢的任何部分 - 你可以假设代码是in the public domain

【讨论】:

  • 这看起来很有趣!我想在JS中重复相同的标签是合法的吗?我希望很快就会将您的答案应用于 jsUnity。并感谢您的点头;)
  • @Ates: ECMA-262, 3rd edition, 12.12:标签被添加到它们前缀的语句的标签集中(即本例中的字符串);嵌套具有相同标签的语句是非法的,例如foo: while(true) { foo: "bar"; }
【解决方案5】:

诀窍基本上是生成一个探测函数,该函数将检查给定名称是否是嵌套(第一级)函数的名称。探测函数使用原始函数的函数体,以代码为前缀,在探测函数范围内检查给定名称。好的,这可以用实际代码更好地解释:

function splitFunction(fn) {
    var tokens =
        /^[\s\r\n]*function[\s\r\n]*([^\(\s\r\n]*?)[\s\r\n]*\([^\)\s\r\n]*\)[\s\r\n]*\{((?:[^}]*\}?)+)\}\s*$/
        .exec(fn);

    if (!tokens) {
        throw "Invalid function.";
    }

    return {
        name: tokens[1],
        body: tokens[2]
    };
}

var probeOutside = function () {
    return eval(
        "typeof $fn$ === \"function\""
        .split("$fn$")
        .join(arguments[0]));
};

function extractFunctions(fn) {
    var fnParts = splitFunction(fn);

    var probeInside = new Function(
        splitFunction(probeOutside).body + fnParts.body);

    var tokens;
    var fns = [];
    var tokenRe = /(\w+)/g;

    while ((tokens = tokenRe.exec(fnParts.body))) {
        var token = tokens[1];

        try {
            if (probeInside(token) && !probeOutside(token)) {
                fns.push(token);
            }
        } catch (e) {
            // ignore token
        }
    }

    return fns;
}

在 Firefox、IE、Safari、Opera 和 Chrome 上运行良好:

function testGlobalFn() {}

function testSuite() {
    function testA() {
        function testNested() {
        }
    }

    // function testComment() {}
    // function testGlobalFn() {}

    function // comments
    testB /* don't matter */
    () // neither does whitespace
    {
        var s = "function testString() {}";
    }
}

document.write(extractFunctions(testSuite));
// writes "testA,testB"

Christoph 编辑,Ates 在线回答:

一些cmets,问题和建议:

  1. 有检查的理由吗

    typeof $fn$ !== "undefined" && $fn$ instanceof Function
    

    而不是使用

    typeof $fn$ === "function"
    

    instanceof 不如使用typeof 安全,因为在帧边界之间传递对象时会失败。我知道 IE 会为某些内置函数返回错误的 typeof 信息,但是 afaik instanceof 在这些情况下也会失败,那么为什么要进行更复杂但更不安全的测试呢?


[AG] 绝对没有正当理由。按照您的建议,我已将其更改为更简单的“typeof === 函数”。


  1. 您将如何防止错误排除在外部范围内存在同名函数的函数,例如

    function foo() {}
    
    function TestSuite() {
        function foo() {}
    }
    

[AG] 我不知道。你能想到什么吗。你觉得哪一个更好? (a) 错误排除内部函数。 (b) 错误地包含外部函数。

我开始认为理想的解决方案是您的解决方案和这种探测方法的结合;找出闭包内的真实函数名称,然后使用探测收集对实际函数的引用(以便可以直接从外部调用它们)。


  1. 也许可以修改您的实现,使函数的主体只需eval()'ed 一次,而不是每个令牌一次,这是相当低效的。当我今天有更多空闲时间时,我可能会尝试看看我能想出什么......

[AG] 请注意,整个函数体不是 eval'd。它只是插入身体顶部的那部分。

[CG] 你的权利——在probeInside的创建过程中函数的主体只被解析一次——你做了一些很好的黑客攻击,那里;)。我今天有一些空闲时间,所以让我们看看我能想出什么......

使用您的解析方法提取真实函数名称的解决方案可以只使用一个 eval 来返回对实际函数的引用数组:

return eval("[" + fnList + "]");

[CG] 这是我想出的。一个额外的好处是外部函数保持不变,因此仍然可以作为内部函数的闭包。只需将代码复制到空白页中,看看它是否有效 - 不保证无错误 ;)

<pre><script>
var extractFunctions = (function() {
    var level, names;

    function tokenize(code) {
        var code = code.split(/\\./).join(''),
            regex = /\bfunction\b|\(|\)|\{|\}|\/\*|\*\/|\/\/|"|'|\n|\s+|\\/mg,
            tokens = [],
            pos = 0;

        for(var matches; matches = regex.exec(code); pos = regex.lastIndex) {
            var match = matches[0],
                matchStart = regex.lastIndex - match.length;

            if(pos < matchStart)
                tokens.push(code.substring(pos, matchStart));

            tokens.push(match);
        }

        if(pos < code.length)
            tokens.push(code.substring(pos));

        return tokens;
    }

    function parse(tokens, callback) {
        for(var i = 0; i < tokens.length; ++i) {
            var j = callback(tokens[i], tokens, i);
            if(j === false) break;
            else if(typeof j === 'number') i = j;
        }
    }

    function skip(tokens, idx, limiter, escapes) {
        while(++idx < tokens.length && tokens[idx] !== limiter)
            if(escapes && tokens[idx] === '\\') ++idx;

        return idx;
    }

    function removeDeclaration(token, tokens, idx) {
        switch(token) {
            case '/*':
            return skip(tokens, idx, '*/');

            case '//':
            return skip(tokens, idx, '\n');

            case ')':
            tokens.splice(0, idx + 1);
            return false;
        }
    }

    function extractTopLevelFunctionNames(token, tokens, idx) {
        switch(token) {
            case '{':
            ++level;
            return;

            case '}':
            --level;
            return;

            case '/*':
            return skip(tokens, idx, '*/');

            case '//':
            return skip(tokens, idx, '\n');

            case '"':
            case '\'':
            return skip(tokens, idx, token, true);

            case 'function':
            if(level === 1) {
                while(++idx < tokens.length) {
                    token = tokens[idx];

                    if(token === '(')
                        return idx;

                    if(/^\s+$/.test(token))
                        continue;

                    if(token === '/*') {
                        idx = skip(tokens, idx, '*/');
                        continue;
                    }

                    if(token === '//') {
                        idx = skip(tokens, idx, '\n');
                        continue;
                    }

                    names.push(token);
                    return idx;
                }
            }
            return;
        }
    }

    function getTopLevelFunctionRefs(func) {
        var tokens = tokenize(func.toString());
        parse(tokens, removeDeclaration);

        names = [], level = 0;
        parse(tokens, extractTopLevelFunctionNames);

        var code = tokens.join('') + '\nthis._refs = [' +
            names.join(',') + '];';

        return (new (new Function(code)))._refs;
    }

    return getTopLevelFunctionRefs;
})();

function testSuite() {
    function testA() {
        function testNested() {
        }
    }

    // function testComment() {}
    // function testGlobalFn() {}

    function // comments
    testB /* don't matter */
    () // neither does whitespace
    {
        var s = "function testString() {}";
    }
}

document.writeln(extractFunctions(testSuite).join('\n---\n'));
</script></pre>

不像 LISP 宏那样优雅,但 JAvaScript 的能力仍然不错;)

【讨论】:

  • 1.为什么不isFnTmp = "typeof $fn$ === \"function\" - instanceof 突破帧边界! - 2.你打算如何处理window.func = function func() {}
  • 3.我不认为这会表现良好(没有基准测试,让我感到羞耻:()-您必须eval()每个令牌的整个函数体!
  • @Christoph:您的问题 #1 应该(至少在实际范围内)通过 probeOutside 添加来处理。
  • @Christoph:#3:您可能误读了代码;每个令牌都会对 typeof 检查进行一次评估。当然,根据给定代码块中标记的数量,这可能很麻烦。但是,性能不是问题,因为这只在测试套件编译时完成。
  • @Ates:您对我编辑您的答案以添加我的问题有疑问吗? cmets 有点限制...
【解决方案6】:
<pre>
<script type="text/javascript">
function someFn() {
    /**
     * Some comment
     */
     function fn1() {
         alert("/*This is not a comment, it's a string literal*/");
     }

     function // keyword
     fn2 // name
     (x, y) // arguments
     {
         /*
         body
         */
     }

     function fn3() {
        alert("this is the word function in a string literal");
     }

     var f = function () { // anonymous, ignore
     };
}

var s = someFn.toString();
// remove inline comments
s = s.replace(/\/\/.*/g, "");
// compact all whitespace to a single space
s = s.replace(/\s{2,}/g, " ");
// remove all block comments, including those in string literals
s = s.replace(/\/\*.*?\*\//g, "");
document.writeln(s);
// remove string literals to avoid false matches with the keyword 'function'
s = s.replace(/'.*?'/g, "");
s = s.replace(/".*?"/g, "");
document.writeln(s);
// find all the function definitions
var matches = s.match(/function(.*?)\(/g);
for (var ii = 1; ii < matches.length; ++ii) {
    // extract the function name
    var funcName = matches[ii].replace(/function(.+)\(/, "$1");
    // remove any remaining leading or trailing whitespace
    funcName = funcName.replace(/\s+$|^\s+/g, "");
    if (funcName === '') {
        // anonymous function, discard
        continue;
    }
    // output the results
    document.writeln('[' + funcName + ']');
}
</script>
</pre>

我确定我错过了一些东西,但是根据您在原始问题中的要求,我认为我已经实现了目标,包括摆脱在字符串文字中找到 function 关键字的可能性。

最后一点,我认为在函数块中修改字符串文字没有任何问题。您的要求是找到函数名称,所以我没有费心尝试保留函数内容。

【讨论】:

  • 我认为如果 cmets 和字符串没有“正确”嵌套,这将中断 - imo 无法手动解析源代码...
  • 您可以假设嵌套是正确的,因为我正在解析一个已经编译(有效)的 JavaScript 函数。
  • @Ates: '嵌套不当'我的意思是像` // " " , /* " */ " `,...
猜你喜欢
  • 2021-08-04
  • 2012-08-28
  • 2012-11-20
  • 2018-11-22
  • 2018-07-02
  • 1970-01-01
  • 1970-01-01
  • 2022-10-21
  • 2011-03-31
相关资源
最近更新 更多