如何从JavaScript文件中提取JavaScript函数

8
我需要从一个脚本文件中提取整个javascript函数。我知道这个函数的名称,但我不知道函数的内容可能是什么。该函数可能嵌套在任意数量的闭包中。
我需要有两个输出值:
1. 我正在查找的命名函数的整个主体。 2. 找到的命名函数已从完整输入脚本中移除。
因此,假设我在此输入脚本中寻找findMe函数:
function() {
  function something(x,y) {
    if (x == true) {
      console.log ("Something says X is true");
      // The regex should not find this:
      console.log ("function findMe(z) { var a; }");
    }
  }
  function findMe(z) {
    if (z == true) {
      console.log ("Something says Z is true");
    }
  }
  findMe(true);
  something(false,"hello");
}();

从这里,我需要以下两个结果值:

  1. The extracted findMe script

    function findMe(z) {
      if (z == true) {
        console.log ("Something says Z is true");
      }
    }
    
  2. The input script with the findMe function removed

    function() {
      function something(x,y) {
        if (x == true) {
          console.log ("Something says X is true");
          // The regex should not find this:
          console.log ("function findMe(z) { var a; }");
        }
      }
      findMe(true);
      something(false,"hello");
    }();
    
我正在处理的问题:
  1. 要查找的脚本正文可能包含任何有效的JavaScript代码。查找此脚本的代码或正则表达式必须能够忽略字符串中的值,多个嵌套的块级等。

  2. 如果要查找的函数定义在字符串内部指定,则应将其忽略。

有关如何完成此类操作的任何建议?

更新:

看起来正则表达式不是解决这个问题的正确方式。我愿意听取可以帮助我完成此任务的解析器的指针。我正在查看Jison,但很想听听其他内容。


1
你需要用JavaScript完成它,还是可以使用其他语言(例如Python)? - Matteo Ceccarello
我刚刚更新了问题,不再是正则表达式特定的。基本上,我正在寻找解决问题的方案,无论这个解决方案是否涉及正则表达式都无关紧要。 - Tauren
也许你可以尝试使用正则表达式查找函数名,然后使用堆栈选择函数体:从找到函数名的文件中解析,当您发现“{”(或其他任何内容)时将其推入堆栈,并在找到“}”时从堆栈中弹出符号。当堆栈变为空时,您已经到达函数体的末尾,完成了操作。这肯定不是高效或极其优雅的解决方案,但它可能是一种解决方法。 - Matteo Ceccarello
在您发布的示例中,如果删除findMe()函数,脚本将会出错,因为仍然有调用它的代码。此外,考虑到您要查找即使嵌入在其他函数中的函数,您确定不会有多个具有您要查找的名称的函数吗? - nnnnnn
@nnnnnn 是的,我知道这似乎是一件奇怪的事情。基本上,我有一堆非常相似的JavaScript文件,它们都定义了相同的函数。每个文件在定义这些公共函数之后都有一些独特的代码。我想从每个文件中提取所有公共函数。然后我会编写一个新文件,开始一个闭包,包括一个公共函数的副本,然后是来自所有文件的所有其他代码。公共函数占用的空间比每个文件的其余部分要多得多,因此这将显着减小可下载的总大小。 - Tauren
显示剩余2条评论
5个回答

3
正则表达式无法做到这一点。您需要的是一种工具,以编译器准确的方式解析JavaScript,构建表示JavaScript代码形状的结构,使您能够找到所需的函数并将其打印出来,并使您能够从该结构中删除函数定义并重新生成剩余的JavaScript文本。
我们的DMS软件重构工具包可以通过其JavaScript前端实现此功能。DMS提供了通用的解析、抽象语法树构建/导航/操作和(有效的!)源文本漂亮打印的功能。JavaScript前端为DMS提供了JavaScript的编译器准确定义。您可以将DMS/JavaScript指向JavaScript文件(甚至各种包含JavaScript的嵌入式脚本标记的动态HTML),让它生成AST。 DMS模式可用于查找您的函数:
  pattern find_my_function(r:type,a: arguments, b:body): declaration
     " \r my_function_name(\a) { \b } ";

DMS可以搜索AST以查找具有指定结构的匹配树;由于这是AST匹配而不是字符串匹配,因此换行、空格、注释和其他微小差异都不会欺骗它。[你没有说如果在不同作用域中有多个函数时该怎么办:你想要哪一个?]
找到匹配后,您可以要求DMS仅打印匹配的代码,这充当提取步骤。您还可以使用重写规则要求DMS删除该函数。
  rule remove_my_function((r:type,a: arguments, b:body): declaration->declaration
     " \r my_function_name(\a) { \b } " -> ";";

然后美化输出AST。DMS会正确地保留所有注释。
这并不意味着它会检查移除函数是否会破坏代码。毕竟,它可能在直接访问作用域中定义的变量的范围内。将其移动到另一个作用域现在意味着它无法引用其变量。
要检测此问题,您不仅需要解析器,还需要具有将代码中的标识符映射到定义和用法的符号表。然后,删除规则必须添加语义条件来检查此内容。DMS提供了使用属性语法从AST构建这样的符号表的机制。
要修复此问题,在删除函数时,可能需要修改函数以接受替换其访问的局部变量的附加参数,并修改调用站点以传递相当于对局部变量的引用。这可以使用一组适度大小的DMS重写规则来实现,这些规则检查符号表。
因此,删除此类函数可能比仅移动代码要复杂得多。

2

如果脚本已经被包含在您的页面中(这是您没有明确说明的),并且该函数是公开可访问的,则可以使用以下代码获取该函数的源代码:

functionXX.toString();

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/toString

其他想法:

1)查看执行JS代码压缩或美化排版的开源代码。在这两种情况下,这些代码片段必须“理解”JS语言以便以容错方式完成工作。我怀疑它不会仅仅使用正则表达式,因为该语言稍微有点复杂。

2)如果您控制服务器上的源代码并且想要修改其中的特定函数,则只需插入一些新的JS代码,用自己的函数在运行时替换该函数。这样,您就让JS编译器为您标识函数,并用自己的版本替换它。

3)对于正则表达式,这是我所做过的,虽然不是绝对可靠,但对我使用的某些构建工具有效:

我运行多个步骤(使用python中的正则表达式):

  1. 删除所有用/*和*/界定的注释。
  2. 删除所有带引号的字符串
  3. 现在,剩下的都是非字符串、非注释的javascript,因此您应该能够直接在函数声明上使用正则表达式
  4. 如果您需要带有字符串和注释的函数源代码,则必须从原始代码中重新构建它,现在您已经知道了函数的开始和结束位置

这是我使用的正则表达式(以python的多行格式表示):

reStr = r"""
    (                               # capture the non-comment portion
        "(?:\\.|[^"\\])*"           # capture double quoted strings
        |
        '(?:\\.|[^'\\])*'           # capture single quoted strings
        |
        (?:[^/\n"']|/[^/*\n"'])+    # any code besides newlines or string literals
        |
        \n                          # newline
    )
    |
    (/\*  (?:[^*]|\*[^/])*   \*/)       # /* comment */
    |
    (?://(.*)$)                     # // single line comment
    $"""    

reMultiStart = r"""         # start of a multiline comment that doesn't terminate on this line
    (
        /\*                 # /* 
        (
            [^\*]           # any character that is not a *
            |               # or
            \*[^/]          # * followed by something that is not a /
        )*                  # any number of these
    )
    $"""

reMultiEnd = r"""           # end of a multiline comment that didn't start on this line
    (
        ^                   # start of the line
        (
            [^\*]           # any character that is not a *
            |               # or
            \*+[^/]         # * followed by something that is not a /
        )*                  # any number of these
        \*/                 # followed by a */
    )
"""

regExSingleKeep = re.compile("// /")                    # lines that have single lines comments that start with "// /" are single line comments we should keep
regExMain = re.compile(reStr, re.VERBOSE)
regExMultiStart = re.compile(reMultiStart, re.VERBOSE)
regExMultiEnd = re.compile(reMultiEnd, re.VERBOSE)

这听起来对我来说有些混乱。你最好解释一下你真正想要解决的问题,这样人们就可以帮助找到更优雅的解决方案来解决实际问题。

谢谢,我知道这个。但在这种情况下,这是在服务器上进行的,我只是处理纯文本。我没有将JavaScript包含在页面中。 - Tauren
@jfriend00,我没有看到有处理JS正则表达式引用的正则表达式。;-) - Qtax
此外,你的注释会在 /* foo **/ 处中断。 - Qtax
这是我在自己的代码中使用的起点(用于部分最小化我的JS,保留最大的调试能力),其中我不会在JS中做破坏它的事情。我在我的答案中说它并不是万无一失的。我只是分享我拥有的正则表达式内容。任何其他人都可以自由地使用它,改进它或分享你拥有的更好的东西。 - jfriend00
1
+1 好主意去看看现有的代码压缩工具和其他代码解析工具。我还建议看看 JSLint。 - davin
显示剩余4条评论

2

我使用纯文本方法(不使用正则表达式)在C#中构建了一个解决方案,它对我有用,可以处理嵌套函数。其基本原理是计算大括号的数量并检查不平衡的闭合大括号。注意:这种方法无法处理大括号作为注释的情况,但是您可以在解析函数边界之前从代码中删除注释以轻松改进此解决方案。

我首先添加了这个扩展方法来提取字符串中所有匹配项的索引(来源:More efficient way to get all indexes of a character in a string

    /// <summary>
    /// Source: https://dev59.com/Imcs5IYBdhLWcg3wiEmK
    /// </summary>
    public static List<int> AllIndexesOf(this string str, string value)
    {
        if (String.IsNullOrEmpty(value))
            throw new ArgumentException("the string to find may not be empty", "value");
        List<int> indexes = new List<int>();
        for (int index = 0; ; index += value.Length)
        {
            index = str.IndexOf(value, index);
            if (index == -1)
                return indexes;
            indexes.Add(index);
        }
    }

我定义了这个结构体方便引用函数边界:

    private struct FuncLimits
    {
        public int StartIndex;
        public int EndIndex;
    }

这是主要函数,我在其中解析边界:
    public void Parse(string file)
    {
        List<FuncLimits> funcLimits = new List<FuncLimits>();

        List<int> allFuncIndices = file.AllIndexesOf("function ");
        List<int> allOpeningBraceIndices = file.AllIndexesOf("{");
        List<int> allClosingBraceIndices = file.AllIndexesOf("}");

        for (int i = 0; i < allFuncIndices.Count; i++)
        {
            int thisIndex = allFuncIndices[i];
            bool functionBoundaryFound = false;

            int testFuncIndex = i;
            int lastIndex = file.Length - 1;

            while (!functionBoundaryFound)
            {
                //find the next function index or last position if this is the last function definition
                int nextIndex = (testFuncIndex < (allFuncIndices.Count - 1)) ? allFuncIndices[testFuncIndex + 1] : lastIndex;

                var q1 = from c in allOpeningBraceIndices where c > thisIndex && c <= nextIndex select c;
                var qTemp = q1.Skip<int>(1); //skip the first element as it is the opening brace for this function

                var q2 = from c in allClosingBraceIndices where c > thisIndex && c <= nextIndex select c;

                int q1Count = qTemp.Count<int>();
                int q2Count = q2.Count<int>();

                if (q1Count == q2Count && nextIndex < lastIndex)
                    functionBoundaryFound = false; //next function is a nested function, move on to the one after this
                else if (q2Count > q1Count)
                {
                    //we found the function boundary... just need to find the closest unbalanced closing brace 
                    FuncLimits funcLim = new FuncLimits();
                    funcLim.StartIndex = q1.ElementAt<int>(0);
                    funcLim.EndIndex = q2.ElementAt<int>(q1Count);
                    funcLimits.Add(funcLim);

                    functionBoundaryFound = true;
                }
                testFuncIndex++;
            }
        }
    }

1

我几乎害怕正则表达式无法完成这项工作。我认为这与尝试使用正则表达式解析XML或HTML相同,这是在该论坛上已经引起了各种宗教争论的话题。

编辑:如果这与尝试解析XML不同,请纠正我。


虽然这并没有回答问题,但我给它+1,因为这将是对正则表达式的滥用。正则表达式从来不是用来解析本质上是递归的输入(如编程语言)的。 - Ruan Mendes
NP完全问题?这与什么有关?Javascript语法是上下文无关的语法,大多数语言也是如此。解析非常确定性,是一个多项式时间操作。此外,您对通用解析的简化是不正确的(它不构成有效的iff,因为它是单向的)。虽然能够解析将解决此问题,但这是解析问题的子问题,甚至可能更容易,甚至可以通过正则表达式解决。 - davin
@davin 你说过JS语法是一种上下文无关文法(因此可以用下推自动机来表示/编写/验证)。正则表达式是正则的,因此等价于有限状态自动机。有限状态自动机无法处理上下文无关文法。 - Hyperboreus
@davin 关于使用正则表达式解析非规则输入,请参考:https://dev59.com/X3I-5IYBdhLWcg3wq6do - Hyperboreus
Hyperboreus,我说JS是一个CFG,是为了解释为什么这个问题与NPC类无关。关于你其他的评论,我认为我已经很清楚了:你假设OP的问题等同于能够解析JS,但实际上并不是这样。正如我所说,你的假设是一种简化,尽管它是错误的。我同意CFG解析通常不能用正则表达式来解决,但是(1)这并不意味着一些带有自由语言的简单正则表达式不够强大,(2)OP的问题不是通用解析。 - davin
显示剩余2条评论

-1

我想你需要使用和构建一个字符串分词器来完成这个任务。

function tokenizer(str){
  var stack = array(); // stack of opening-tokens
  var last = ""; // last opening-token

  // token pairs: subblocks, strings, regex
  var matches = {
    "}":"{",
    "'":"'",
    '"':'"',
    "/":"/"
  };

  // start with function declaration
  var needle = str.match(/function[ ]+findme\([^\)]*\)[^\{]*\{/);

  // move everything before needle to result
  var result += str.slice(0,str.indexOf(needle));
  // everithing after needle goes to the stream that will be parsed
  var stream = str.slice(str.indexOf(needle)+needle.length);

  // init stack
  stack.push("{");
  last = "{";

  // while still in this function
  while(stack.length > 0){

    // determine next token
    needle = stream.match(/(?:\{|\}|"|'|\/|\\)/); 

    if(needle == "\\"){
      // if this is an escape character => remove escaped character
      stream = stream.slice(stream.indexOf(needle)+2);
      continue;

    }else if(last == matches[needle]){
      // if this ends something pop stack and set last
      stack.pop();
      last = stack[stack.length-1];

    }else if(last == "{"){  
      // if we are not inside a string (last either " or ' or /)
      // push needle to stack
      stack.push(needle);
      last = needle;
    }

    // cut away including token
    stream = stream.slice(stream.indexOf(needle)+1);
  }

  return result + stream;
}

哦,我忘记了注释的标记...但我猜你现在已经有了它是如何工作的想法...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接