有没有一种非eval的方法来创建一个函数并且这个函数名是在运行时决定的?

41

有没有一种方法可以在运行时创建一个真实名称的函数,而不使用eval,只使用纯 JavaScript?(因此,不能使用生成的script元素,因为这些元素特定于浏览器环境[在许多方面上,它们实际上是eval的化身];不能使用某个特定 JavaScript 引擎的非标准功能等)

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

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

尽管对象属性有名称,但函数没有。匿名函数对很多事情都没问题,但不符合我此处的需求。我希望该函数有一个名称(例如,在调试器中出现在调用栈中等)。

3个回答

53

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. Live with an anonymous function instead. Many modern engines will show a useful name (e.g., in call stacks and such) if you have a nice, unambiguous var name = function() { ... }; expression (showing the name of the variable), even though technically the function doesn't have a name. In ES6, functions created that way will actually have names if they can be inferred from the context. Either way, though, if you want a truly runtime-defined name (a name coming from a variable), you're pretty much stuck.

  2. Use eval. eval is evil when you can avoid it, but with strings you're in total control of, in a scope you control, with an understanding of the costs (you're firing up a JavaScript parser), to do something you cannot do otherwise (as in this case), it's fine provided you really need to do that thing. But if you're not in control of the string or scope, or you don't want the cost, you'll have to live with an anonymous function.

    Here's how the eval option looks:

    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

    That creates a function with the name we come up with at runtime without leaking the name into the containing scope (and without triggering the flawed handling of named function expressions in IE8 and earlier), assigning a reference to that function to f. (And it formats the code nicely so single-stepping through it in a debugger is easy.)

    This didn't used to correctly assign the name (surprisingly) in older versions of Firefox. As of the current version of their JavaScript engine in Firefox 29, it does.

    Because that uses eval, the function you create has access to the scope in which it was created, which is important if you're a tidy coder who avoids global symbols. So this works, for instance:

    (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. Use the Function constructor, as demonstrated in this article by Marcos Cáceres:

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

    Live example | Live source

    There we create a temporary anonymous function (the one created via the Function constructor) and call it; that temporary anonymous function creates a named function using a named function expression. That will trigger the flawed handle of named function expressions in IE8 and earlier, but it doesn't matter, because the side-effects of that are limited to the temporary function.

    This is shorter than the eval version, but has an issue: Functions created via the Function constructor do not have access to the scope in which they were created. So the example above using display would fail, because display wouldn't be in-scope for the created function. (Here's an example of it failing. Source). So not an option for tidy coders avoiding global symbols, but useful for those times when you want to disassociate the generated function from the scope in which you're generating it.


2
嗯,我明白你的观点了。好吧,我会考虑避免使用匿名函数。谢谢! - PiTheNumber
1
@Mike'Pomax'Kamermans:有两个问题。 :-) 1. 你不能给new Function函数一个真实的名称,2. 你可以通过将eval包装在try/catch块中来捕获语法错误,就像使用new Function一样。在这方面没有区别。 - T.J. Crowder
1
@DhruvPathak:这是一篇非常有用的文章,但它已经过时了几年。现在唯一重要的NFE问题仅存在于IE8中;所有其他引擎(包括IE9+)都已经解决了这些问题。更多信息请参见Double-take - T.J. Crowder
1
哦,计算属性的使用很好啊 :-) 我希望能看到这个作为一个单独的答案,这样我就可以单独点赞了... - Bergi
1
对于 evalnew Function,我建议使用 function replaceHere(){...}.toString() 而不是多行字符串。我还见过它被用在一个用户脚本中,通过创建一个新的脚本元素将其注入到页面中。 - John Dvorak
显示剩余14条评论

10

这是我一段时间前想出的实用函数。它使用了@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");

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

这将实现与被接受的答案相同的效果

console.log(f.name) // 新名称

然而,当打印函数时,两者都没有显示“新名称”console.log(f) // test(){}


在节点(15)下,我使用 console.log 看到了 "[Function: New Name]" 和 "New Name"。现在使用 console.log('%s', f) 仍然会打印出原始源代码 "function test(){}",但这是预期的。你的答案很好。 - Shenme

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