诀窍基本上是生成一个探测函数,该函数将检查给定名称是否是嵌套(第一级)函数的名称。探测函数使用原始函数的函数体,以代码为前缀,在探测函数范围内检查给定名称。好的,这可以用实际代码更好地解释:
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,问题和建议:
-
有检查的理由吗
typeof $fn$ !== "undefined" && $fn$ instanceof Function
而不是使用
typeof $fn$ === "function"
instanceof 不如使用typeof 安全,因为在帧边界之间传递对象时会失败。我知道 IE 会为某些内置函数返回错误的 typeof 信息,但是 afaik instanceof 在这些情况下也会失败,那么为什么要进行更复杂但更不安全的测试呢?
[AG] 绝对没有正当理由。按照您的建议,我已将其更改为更简单的“typeof === 函数”。
-
您将如何防止错误排除在外部范围内存在同名函数的函数,例如
function foo() {}
function TestSuite() {
function foo() {}
}
[AG] 我不知道。你能想到什么吗。你觉得哪一个更好? (a) 错误排除内部函数。 (b) 错误地包含外部函数。
我开始认为理想的解决方案是您的解决方案和这种探测方法的结合;找出闭包内的真实函数名称,然后使用探测收集对实际函数的引用(以便可以直接从外部调用它们)。
- 也许可以修改您的实现,使函数的主体只需
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 的能力仍然不错;)