从JavaScript函数中提取嵌套函数名称

时间:2009-02-05 19:07:59

标签: javascript regex parsing function

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

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

测试对象

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

document.write(fn.toString());

结果

Browser      post-keyword  post-name  post-parens  inside
-----------  ------------  ---------  -----------  --------
 Firefox      No            No         No           No
 Safari       No            No         No           No
 Chrome       No            No         Yes          Yes
 IE           Yes           Yes        Yes          Yes
 Opera        Yes           Yes        Yes          Yes

我正在寻找一种从给定函数中提取嵌套函数名的跨浏览器方式。解决方案应该能够从以下函数中提取“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
     };
}

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

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

Update2:如果你想知道背后的动机:我正在开发一个名为jsUnity的新的JavaScript单元测试框架。有几种不同的格式可以编写测试和测试套件。其中一个是功能:

function myTests() {
    function setUp() {
    }

    function tearDown() {
    }

    function testSomething() {
    }

    function testSomethingElse() {
    }
}

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

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

6 个答案:

答案 0 :(得分:3)

化妆品更改和错误修正

正则表达式必须读取\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;
}

答案 1 :(得分:3)

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

其他解决方案只是正则表达式攻击,导致无法维护/不可读的代码,并可能在特定情况下隐藏解析错误。

作为旁注,我不确定你为什么不以不同的方式(一组函数?)指定产品中的单元测试函数列表。

答案 2 :(得分:1)

如果您定义了以下测试,那会很重要吗?

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进行测试。

答案 3 :(得分:1)

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

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

随意删除您喜欢的任何部分 - 您可以假设代码为in the public domain

答案 4 :(得分:1)

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

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提供内联答案:

一些意见,问题和建议:

  1. 是否有理由检查

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

    而不是使用

    typeof $fn$ === "function"
    

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


  2. [AG]完全没有合理的理由。我已经按照你的建议把它改成了更简单的“typeof === function”。


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

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

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

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


      1. 可能可以修改您的实现,以便函数的主体只需要eval()一次编辑,而不是每个标记一次,这是相当低效的。当我今天有更多的空闲时间时,可能试图看看我能想出什么......

      2. [AG]请注意,整个函数体未被评估。它只是插在身体顶部的位。

        [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-macros那么优雅,但仍然很好JAvaScript的能力;)

答案 5 :(得分:0)

<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关键字的可能性。

最后一点,我没有看到在功能块中修改字符串文字有任何问题。您的要求是找到函数名称,因此我不打算尝试保留函数内容。