能否不使用eval或Function构造函数动态创建含指定参数的函数?
这个问题和Stack Overflow上的某个Python问题逻辑类似,但针对JavaScript环境。我接触到一个依赖nodejs-depd的库,它通过new Function()动态生成包装函数,给原函数添加弃用提示,核心代码如下:
function wrapfunction(fn, message) { if (typeof fn !== "function") { throw new TypeError("argument fn must be a function"); } var args = createArgumentsString(fn.length); var stack = getStack(); var site = callSiteLocation(stack[1]); site.name = fn.name; var deprecatedFn = new Function( "fn", "log", "deprecate", "message", "site", '"use strict"\n' + "return function (" + args + ") {" + "log.call(deprecate, message, site)\n" + "return fn.apply(this, arguments)\n" + "}" )(fn, log, this, message, site); return deprecatedFn; }
这种方式虽然能生成和原函数参数数量完全匹配的包装函数(比如原函数有2个参数,包装函数就会定义为function(arg0, arg1) { ... }),但存在安全风险。我想知道:能不能不使用eval或Function构造函数,实现同样的效果?哪怕结论是“不可能”也可以接受。
回答
核心结论
如果要求包装后的函数完全复刻原函数的参数列表表现(包括function.length属性、toString()输出的参数定义),同时不使用eval或new Function,那确实不可能。
为什么原代码要这么做?
原库用new Function的核心目的,是让包装函数的参数数量、toString输出和原函数完全一致——这两个特性是普通静态定义的包装函数无法动态适配的。比如你用function(...args)做包装,它的length属性是0,而原函数如果是2个参数,length就是2;同时toString()会显示剩余参数语法,和原函数的参数列表差异明显。
可行的妥协方案(不追求100%参数表现一致)
如果你的场景不需要严格复刻原函数的参数元数据,有两种可靠的替代方案:
1. 剩余参数包装(最简单)
直接用ES6剩余参数实现包装,功能完全正常,只是参数元数据和原函数有差异:
function wrapFunction(fn, message) { if (typeof fn !== "function") { throw new TypeError("argument fn must be a function"); } const stack = getStack(); const site = callSiteLocation(stack[1]); site.name = fn.name; return function(...args) { log.call(deprecate, message, site); return fn.apply(this, args); }; }
- 缺点:包装函数的
length为0,toString()显示(...args),和原函数参数列表不一致; - 适用场景:只要弃用提示能正常触发、函数功能正常,不依赖函数参数元数据的场景。
2. 用Proxy模拟参数元数据(ES6+)
通过Proxy拦截包装函数的length和toString(),模拟原函数的参数表现,同时用剩余参数做实际逻辑:
function wrapFunction(fn, message) { if (typeof fn !== "function") { throw new TypeError("argument fn must be a function"); } const stack = getStack(); const site = callSiteLocation(stack[1]); site.name = fn.name; const wrapper = function(...args) { log.call(deprecate, message, site); return fn.apply(this, args); }; return new Proxy(wrapper, { get(target, prop) { if (prop === 'length') { return fn.length; } if (prop === 'toString') { // 假设createArgumentsString函数能生成arg0,arg1这类参数字符串 return () => fn.toString().replace(/^function[^(]*\(/, `function (${createArgumentsString(fn.length)})`); } return Reflect.get(target, prop); } }); }
- 优点:能模拟原函数的
length属性和toString()输出; - 缺点:这只是“模拟”而非真正的参数定义,比如用
new调用构造函数、或依赖底层参数元数据的工具可能出现异常,且Proxy有轻微性能开销。
为什么无法完全替代new Function?
JavaScript中,函数的参数列表是函数对象的固有特性,由定义时的语法直接决定——运行时无法修改一个已有函数的参数列表结构,只能通过动态生成代码的方式(eval/new Function)创建参数数量匹配的新函数。
内容的提问来源于stack exchange,提问作者mikemaccana

