代码之家  ›  专栏  ›  技术社区  ›  Ates Goral

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

  •  7
  • Ates Goral  · 技术社区  · 16 年前

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

    一个简单的正则表达式 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
    

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

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

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

    更新2: 如果您想知道这背后的动机:我正在开发一个新的JavaScript单元测试框架,名为 jsUnity

    function myTests() {
        function setUp() {
        }
    
        function tearDown() {
        }
    
        function testSomething() {
        }
    
        function testSomethingElse() {
        }
    }
    

    由于函数隐藏在闭包中,因此我无法从函数外部调用它们。因此,我将外部函数转换为字符串,提取函数名,在底部附加一条“now run the gived internal function”语句,并使用new将其重新编译为函数 Function() . 如果测试函数中有注释,那么提取函数名和避免误报就变得很困难。因此我请求SO社区的帮助。。。

    更新3: a new solution 这不需要对代码进行大量的语义处理。我使用原始源本身来探测第一级函数。

    6 回复  |  直到 7 年前
        1
  •  3
  •   Christoph    16 年前

    外观变化和错误修复

    必须 阅读 \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;
    }
    
        2
  •  3
  •   friol    16 年前

    学术上正确的处理方法是为Javascript子集(函数定义)创建一个lexer和解析器,该子集由形式语法生成(参见 this link

    看看 JS/CC ,用于Javascript解析器生成器。

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

    顺便说一句,我不太明白为什么不以不同的方式(函数数组?)指定产品中的单元测试函数列表。

        3
  •  1
  •   Mehmet Duran    16 年前

    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
  •  1
  •   Christoph    16 年前

    jsUnity . 当我看到我喜欢的东西(并且有足够的空闲时间)时,我会尝试以一种更适合我需要的方式重新实现它(也称为“非发明于此”综合症)。

    我的努力成果如中所述 this article ,可以找到代码 here .

    in the public domain .

        5
  •  1
  •   Ates Goral    16 年前

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

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

    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 因为它在帧边界之间传递对象时会失败。我知道你回错了 类型 运算符 在这些情况下也会失败,那么为什么要进行更复杂但不太安全的测试呢?


    [AG]这绝对没有合法的理由。我把它改成了你建议的更简单的“typeof==函数”。


    1. 如何防止错误排除外部作用域中存在同名函数的函数,例如。

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

    我不知道。你能想到什么吗。你认为哪一个更好?(a) 内部函数的错误排除。(b) 外部函数的Wronfgul包含。


    1. 可以修改您的实现,以便只需修改函数体 eval() “每个令牌只执行一次,而不是一次,这是相当低效的。我 可以

    [CG]您的右侧-在创建 probeInside -你做了一些很好的黑客攻击,那里;)。我今天有一些空闲时间,让我们看看我能想出什么。。。

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

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

    <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的功能仍然很好;)

        6
  •  0
  •   Grant Wagner    16 年前
    <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