Извлечение имен вложенных функций из функции 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:
некоторые комментарии, вопросы и предложения:
-
есть ли причина для проверки
typeof $fn$ !== "undefined" && $fn$ instanceof Function
вместо
typeof $fn$ === "function"
instanceof
это менее безопасно, чем использованиеtypeof
потому что он потерпит неудачу при передаче объектов между границами кадра. Я знаю, что IE возвращает неправильноtypeof
информация для некоторых встроенных функции, но емнипinstanceof
не удастся, и в этих случаях, так почему более сложный, но менее безопасный тест?
[AG] для этого не было абсолютно никаких законных оснований. Я изменил его на более простой "typeof === function", как вы предложили.
-
как вы собираетесь предотвратить неправомерное исключение функций, для которых функция с тем же именем существует во внешней области, например,
function foo() {} function TestSuite() { function foo() {} }
[AG] понятия не имею. Ты можешь что-нибудь придумать? Как ты думаешь, что лучше? а) Неправомерное исключение внутренней функции. b) Вронфгульское включение функции извне.
я начал думать, что идеальным решением будет комбинация вашего решения и этого зондирующего подхода; выяснить реальные имена функций, которые находятся внутри закрытия, а затем использовать зондирование для сбора ссылок на фактические функции (так что их можно напрямую вызывать извне).
- возможно, можно изменить вашу реализацию так, что тело функции должно быть только
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
ключевое слово в строковые литералы.
последний момент, я не вижу никаких проблем с искажением строковых литералов в функциональных блоках. Ваше требование состояло в том, чтобы найти имена функций, поэтому я не стал пытаться сохранить содержимое функции.