是否有任何非 eval 方法来创建具有运行时确定名称的函数?

IT技术 javascript
2021-01-17 16:14:15

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

请注意,我特别问有关的变量或有名称,如性质引用匿名函数:

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

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

3个回答

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

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

在 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() { },不需要方法语法,函数语法很好。如果您想以这种方式创建构造函数,这很方便:

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


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

不。如果没有构造函数eval或其表亲,你不能这样做Function您的选择是:

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

  2. 使用eval.

  3. 使用Function构造函数。

细节:

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

  2. 使用eval. 当你可以避免它时eval是邪恶的,但是对于字符串,你可以完全控制,在你控制的范围内,了解成本(你正在启动一个 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" +
        "})();"
    );
    

    活生生的例子| 直播源

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

    这并没有用于在旧版本的 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构造函数,如Marcos Cáceres这篇文章中所演示的

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

    活生生的例子| 直播源

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

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

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

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

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

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

将完成与接受的答案相同的任务

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

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

在节点 (15) 下,我看到“[功能:新名称]”和“新名称”使用console.log. 现在 usingconsole.log('%s', f)仍然打印出原始源“function test(){}”,但这是意料之中的。你的回答很好。
2021-04-09 16:14:15