【问题标题】:Is there any non-eval way to create a function with a runtime-determined name?是否有任何非评估方法来创建具有运行时确定名称的函数?
【发布时间】:2012-03-17 18:05:42
【问题描述】:

有没有什么方法可以创建一个在运行时确定的具有真实姓名的函数,而不使用eval,并且只使用纯JavaScript? (因此,没有生成 script 元素,因为它们是特定于浏览器环境的[并且在许多方面无论如何都会伪装成 eval];不使用特定 JavaScript 引擎的非标准功能等)

请注意,我特别不是询问由具有名称的变量或属性引用的匿名函数,例如:

// NOT this
var name = /* ...come up with the name... */;
var obj = {};
obj[name] = function() { /* ... */ };

在那里,虽然对象属性有名称,但 函数 没有。匿名函数适用于很多事情,但不是我在这里寻找的。我希望函数有一个名称(例如,显示在调试器的调用堆栈中等)。

【问题讨论】:

    标签: javascript


    【解决方案1】:

    ECMAScript 2015+(又名“ES6”)的答案

    是的。从 ES2015 开始,由分配给对象属性的匿名函数表达式创建的函数采用该对象属性的名称。这在所有现代浏览器中都实现了,尽管 Edge 和 Safari 不在堆栈跟踪中使用该名称。我们可以将它与另一个 ES2015 特性(计算属性名称)结合使用来命名一个没有new Functioneval 的函数。

    在 ES2015 中,这会创建一个名为“foo###”的函数,其中 ### 是 1-3 位:

    const dynamicName = "foo" + Math.floor(Math.random() * 1000);
    const obj = {
      [dynamicName]() {
        throw new Error();
      }
    };
    const f = obj[dynamicName];
    // See its `name` property
    console.log("Function's `name` property: " + f.name + " (see compatibility note)");
    // We can see whether it has a name in stack traces via an exception
    try {
      f();
    } catch (e) {
      console.log(e.stack);
    }

    它也适用于[dynamicName]: function() { },不需要方法语法,函数语法很好。如果您想以这种方式创建构造函数,这很方便:

    const dynamicName = "Foo" + Math.floor(Math.random() * 1000);
    const obj = {
        [dynamicName]: function(throwError = false) {
            if (throwError) {
                throw new Error();
            }
        }
    };
    const F = obj[dynamicName];
    // See its `name` property
    console.log("Function's `name` property: " + F.name + " (see compatibility note)");
    // We can see whether it has a name in stack traces via an exception
    try {
      new F(true);
    } catch (e) {
      console.log(e.stack);
    }
    // And we can see it works as a constructor:
    const inst = new F();
    console.log(inst instanceof F); // true

    当然,这是ES2015+,所以你也可以使用class创建构造函数[dynamicName]: class { }

    const dynamicName = "Foo" + Math.floor(Math.random() * 1000);
    const obj = {
        [dynamicName]: class {
            constructor(throwError = false) {
                if (throwError) {
                    throw new Error();
                }
            }
        }
    };
    const F = obj[dynamicName];
    // See its `name` property
    console.log("Function's `name` property: " + F.name + " (see compatibility note)");
    // We can see whether it has a name in stack traces via an exception
    try {
      new F(true);
    } catch (e) {
      console.log(e.stack);
    }
    // And we can see it works as a constructor:
    const inst = new F();
    console.log(inst instanceof F); // true

    ECMAScript 5 的答案 (从 2012 年开始)

    没有。如果没有 eval 或其表亲 Function 构造函数,您将无法做到这一点。您的选择是:

    1. 改为使用匿名函数。现代引擎会做一些事情来帮助调试这些。

    2. 使用eval

    3. 使用Function 构造函数。

    详情:

    1. 改为使用匿名函数。如果您有一个很好的、明确的var name = function() { ... }; 表达式(显示变量的名称),许多现代引擎将显示一个有用的名称(例如,在调用堆栈等中),即使 技术上 该函数没有没有名字。在 ES6 中,如果可以从上下文中推断出以这种方式创建的函数,它们实际上将具有名称。不过,无论哪种方式,如果您想要一个真正由运行时定义的名称(一个来自变量的名称),那么您几乎会陷入困境。

    2. 使用evaleval 是邪恶的当你可以避免它时,但你可以完全控制字符串,在你控制的范围内,了解成本(你正在启动一个 JavaScript 解析器),做一些你不能做的事情(就像在这种情况下),只要你真的需要做那件事就可以了。但是,如果您无法控制字符串或范围,或者您不想要成本,那么您将不得不使用匿名函数。

      eval 选项的外观如下:

      var name = /* ...come up with the name... */;
      var f = eval(
          "(function() {\n" +
          "   function " + name + "() {\n" +
          "       console.log('Hi');\n" +
          "   }\n" +
          "   return " + name + ";\n" +
          "})();"
      );
      

      Live example | Live source

      这将创建一个具有我们在运行时提出的名称的函数,而不会将名称泄漏到包含范围中(并且不会触发 IE8 和更早版本中命名函数表达式的错误处理),将该函数的引用分配给 @987654346 @。 (而且它很好地格式化了代码,因此在调试器中单步执行它很容易。)

      这在旧版本的 Firefox 中不能正确分配名称(令人惊讶的是)。从 Firefox 29 中的 JavaScript 引擎的当前版本开始,确实如此。

      因为使用eval,所以您创建的函数可以访问创建它的范围,如果您是一个避免全局符号的整洁编码人员,这一点很重要。所以这有效,例如:

      (function() {
          function display(msg) {
              var p = document.createElement('p');
              p.innerHTML = String(msg);
              document.body.appendChild(p);
          }
      
          var name = /* ...come up with the name... */;
          var f = eval(
              "(function() {\n" +
              "   function " + name + "() {\n" +
              "       display('Hi');\n" +         // <=== Change here to use the
              "   }\n" +                          //      function above
              "   return " + name + ";\n" +
              "})();"
          );
      })();
      
    3. 使用Function 构造函数,如this article by Marcos Cáceres 所示:

      var f = new Function(
          "return function " + name + "() {\n" +
          "    display('Hi!');\n" +
          "    debugger;\n" +
          "};"
      )();
      

      Live example | Live source

      我们在这里创建一个临时匿名函数(通过Function 构造函数创建的那个)并调用它;该临时匿名函数使用命名函数表达式创建命名函数。 触发 IE8 及更早版本中命名函数表达式的有缺陷句柄,但这并不重要,因为其副作用仅限于临时函数。

      这比eval 版本短,但有一个问题:通过Function 构造函数创建的函数不能访问它们创建的范围。所以上面使用display 的例子会失败,因为display 不在创建函数的范围内。 (Here's an example 失败了。Source)。因此,对于避免使用全局符号的整洁编码人员来说,这不是一个选项,但对于那些想要将生成的函数与生成它的范围解除关联的时候很有用。

    【讨论】:

    • 嗯,我明白了。好吧,我会考虑避免使用匿名函数。谢谢!
    • @Mike'Pomax'Kamermans:这有两个问题。 :-) 1. 你不能用new Function 给函数一个真实的名字,并且 2. 你可以通过将eval 包装在try/catch 块中来捕获语法错误,就像你可以用new Function。在这方面没有区别。
    • @DhruvPathak:在当时,这是一篇非常非常有用的文章。它已经过时了几年。剩下的唯一重要的 NFE 问题是在 IE8 中;所有其他引擎(包括 IE9+)都正确。更多Double-take
    • 哦,计算属性的用法很好 :-) 我很喜欢在单独的答案中看到这一点,这样我就可以单独投票了……
    • 用于evalnew Function。我建议function replaceHere(){...}.toString() 而不是多行字符串。我还看到它在用户脚本中使用,该用户脚本通过创建新的脚本元素将自身注入页面。
    【解决方案2】:

    这是我前段时间想出的一个实用函数。它使用@T.J.Crowder 的出色答案中概述的Function 构造函数技术,但改进了其缺点并允许对新函数的范围进行细粒度控制。

    function NamedFunction(name, args, body, scope, values) {
        if (typeof args == "string")
            values = scope, scope = body, body = args, args = [];
        if (!Array.isArray(scope) || !Array.isArray(values)) {
            if (typeof scope == "object") {
                var keys = Object.keys(scope);
                values = keys.map(function(p) { return scope[p]; });
                scope = keys;
            } else {
                values = [];
                scope = [];
            }
        }
        return Function(scope, "function "+name+"("+args.join(", ")+") {\n"+body+"\n}\nreturn "+name+";").apply(null, values);
    };
    

    它可以让您保持整洁避免通过eval 完全访问您的范围,例如在上述场景中:

    var f = NamedFunction("fancyname", ["hi"], "display(hi);", {display:display});
    f.toString(); // "function fancyname(hi) {
                  // display(hi);
                  // }"
    f("Hi");
    

    【讨论】:

      【解决方案3】:

      还有

      let f = function test(){};
      Object.defineProperty(f, "name", { value: "New Name" });
      

      将完成与the accepted answer 相同的操作

      console.log(f.name) // New Name

      但是在打印函数时都不显示“新名称” console.log(f) // test(){}

      【讨论】:

      • 在节点 (15) 下,我看到使用 console.log 的“[Function: New Name]”和“New Name”。现在使用console.log('%s', f) 仍然会打印出原始源“function test(){}”,但这是意料之中的。你的回答很好。
      猜你喜欢
      • 2018-07-28
      • 2013-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多