Извлечение имен вложенных функций из функции JavaScript

учитывая функцию, я пытаюсь выяснить имена вложенных функций в ней (только на одном уровне).

простое регулярное выражение против toString() работало, пока я не начал использовать функции с комментариями в них. Оказывается, некоторые браузеры хранят части исходного кода, в то время как другие восстанавливают источник из того, что скомпилировано; выход toString() может содержать комментарии исходного кода в некоторых браузерах. В стороне, вот мои выводы:

6 ответов


косметические изменения и исправление

регулярное выражение должны читать \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;
}

академически правильным способом обработки этого было бы создание лексера и парсера для подмножества Javascript (определение функции), генерируемого формальной грамматикой (см. этой ссылке по теме, например).

Взгляните на JS / CC, скрипт парсер-генератор.

другие решения-это просто хаки регулярных выражений, которые приводят к недостижимому / нечитаемому коду и, возможно, к скрытым ошибкам синтаксического анализа, в частности случаи.

в качестве примечания я не уверен, что понимаю, почему вы не указываете список функций модульного теста в своем продукте по-другому (массив функций?).


было бы, если бы вы определили свои тесты, как:

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 таким образом.


Мне нравится, что вы делаете с jsUnity. И когда я вижу что-то, что мне нравится (и у меня достаточно свободного времени;)), я пытаюсь переосмыслить это таким образом, который лучше соответствует моим потребностям (также известный как синдром "не изобретенный здесь").

результат моих усилий описан в в этой статье, код можно найти здесь.

Не стесняйтесь вырывать любые части, которые вам нравятся - вы можете предположить, что код в общественном домен.


трюк состоит в том, чтобы в основном генерировать функцию зонда, которая будет проверять, является ли данное имя именем вложенной (первого уровня) функции. Функция probe использует тело функции исходной функции с префиксом code для проверки данного имени в рамках функции 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;
}

отлично работает против следующих в Firefox, IE, Safari, Opera и Хром:

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"

редактировать Кристоф, с встроенными ответами Ates:

некоторые комментарии, вопросы и предложения:

  1. есть ли причина для проверки

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

    вместо

    typeof $fn$ === "function"
    

    instanceof это менее безопасно, чем использование typeof потому что он потерпит неудачу при передаче объектов между границами кадра. Я знаю, что IE возвращает неправильно typeof информация для некоторых встроенных функции, но емнип instanceof не удастся, и в этих случаях, так почему более сложный, но менее безопасный тест?


[AG] для этого не было абсолютно никаких законных оснований. Я изменил его на более простой "typeof === function", как вы предложили.


  1. как вы собираетесь предотвратить неправомерное исключение функций, для которых функция с тем же именем существует во внешней области, например,

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

[AG] понятия не имею. Ты можешь что-нибудь придумать? Как ты думаешь, что лучше? а) Неправомерное исключение внутренней функции. b) Вронфгульское включение функции извне.

я начал думать, что идеальным решением будет комбинация вашего решения и этого зондирующего подхода; выяснить реальные имена функций, которые находятся внутри закрытия, а затем использовать зондирование для сбора ссылок на фактические функции (так что их можно напрямую вызывать извне).


  1. возможно, можно изменить вашу реализацию так, что тело функции должно быть только eval()'ed один раз и не один раз за токен, что довольно неэффективно. Я!--52-->может попробуйте посмотреть, что я могу придумать, когда у меня будет больше свободного времени сегодня...

[AG] обратите внимание, что все тело функции не eval'D. Это только бит, который вставлен в верхнюю часть тело.

[CG] ваше право-тело функции анализируется только один раз во время создания probeInside - вы сделали хороший взлом, там ;). У меня сегодня есть свободное время, так что посмотрим, что я смогу придумать...

решение, которое использует ваш метод синтаксического анализа для извлечения реальных имен функций, может просто использовать один eval для возврата массива ссылок на фактические функции:

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

[CG]вот с чем я пришел. Дополнительным бонусом является что внешняя функция остается неповрежденной и, таким образом, все еще может действовать как закрытие вокруг внутренних функций. Просто скопируйте код на пустую страницу и посмотрите, работает ли он - никаких гарантий на ошибку-freelessness;)

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


<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(.+)\(/, "");
    // 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 ключевое слово в строковые литералы.

последний момент, я не вижу никаких проблем с искажением строковых литералов в функциональных блоках. Ваше требование состояло в том, чтобы найти имена функций, поэтому я не стал пытаться сохранить содержимое функции.