◆

JavaScript new Function 构造函数与作用域

2023-02-02 01:370

new Function 是 JavaScript 里第三种创建函数的方式,前两种是函数声明和函数表达式。它的特别之处在于函数体是以字符串形式传进去的,等到运行时才被解析成代码。这意味着我们可以在程序跑起来之后再决定函数里写什么,模板引擎、表达式求值器、把后端下发的规则变成可执行逻辑,这类场景基本都绕不开它。

代价是它的作用域行为和另外两种写法完全不一样,这也是最容易被记错的地方。下面先看这个差异,再看它的语法细节和该不该用的判断。

只能访问全局作用域

由 Function 构造函数创建的函数不会创建当前环境的闭包,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造函数创建时所在的作用域的变量。这一点与使用 eval() 执行创建函数的代码不同。

var x = 10;

function createFunction1() {
  var x = 20;
  return new Function('return x;'); // 这里的 x 指向最上面全局作用域内的 x
}

function createFunction2() {
  var x = 20;

  function f() {
    return x; // 这里的 x 指向上方本地作用域内的 x
  }

  return f;
}

var f1 = createFunction1();
console.log(f1()); // 10
var f2 = createFunction2();
console.log(f2()); // 20

createFunction1 返回的函数里那个 x,在解析时压根就没有 createFunction1 这层作用域可以查,它的作用域链只有全局,所以拿到 10。createFunction2 里的 f 是正常的函数声明,捕获了外层的 x,拿到 20。

这个特性在打包压缩后会变成一个实际的坑:压缩工具会重命名局部变量,但它没法知道字符串里的标识符指向什么,所以不会跟着改。原本靠外层变量工作的字符串代码,压缩后会因为找不到名字而抛 ReferenceError。反过来说,正因为 new Function 只看全局,它的行为在压缩前后是一致的,这一点比 eval 反而更可控。

语法与参数传递

构造函数接收若干个字符串:前面的都是形参名,最后一个是函数体。new 可以省略,Function(...) 和 new Function(...) 效果相同。

// 形参可以分开写,也可以用逗号写在一个字符串里
const sum1 = new Function('a', 'b', 'return a + b;');
const sum2 = new Function('a, b', 'return a + b;');

console.log(sum1(1, 2)); // 3
console.log(sum2(1, 2)); // 3

// 不传形参时只有函数体
const now = new Function('return Date.now();');
console.log(typeof now()); // "number"

既然拿不到外层变量,需要传数据进去就只能走两条路:一是声明成形参在调用时传入,二是显式挂到全局。前者更可控,应该作为默认选择:

const scope = { price: 10, quantity: 2 };

// 把外部数据声明为形参,调用时按名字传进去
const compute = new Function('price', 'quantity', 'return price * quantity;');
console.log(compute(scope.price, scope.quantity)); // 20

// 参数多的时候,直接把整个上下文对象传进来更省事
const compute2 = new Function('ctx', 'return ctx.price * ctx.quantity;');
console.log(compute2(scope)); // 20

与 eval 的区别

两者都会在运行时把字符串当代码执行,区别在作用域和返回值:

  1. eval 在调用它的那个作用域里执行,能读写外层的局部变量;new Function 只有全局作用域。
  2. eval 返回的是最后一个表达式的值,执行完就结束;new Function 返回一个可以反复调用的函数对象,解析只发生一次。
  3. eval 能污染当前作用域(非严格模式下可以在里面声明变量影响外部),new Function 做不到,隔离性更好。

所以如果确实需要动态执行代码,优先选 new Function:既避免了作用域被意外修改,又能把解析开销摊到多次调用上。

什么时候不该用

new Function 和 eval 一样属于动态代码执行,有两个硬性限制需要提前确认:

  • 绝不能拼接不可信输入。用户输入、URL 参数、第三方接口返回的字符串直接进函数体等于把执行权交出去,这是标准的代码注入。
  • CSP 会直接禁掉它。如果页面的 Content-Security-Policy 没有开 'unsafe-eval',new Function 会抛 EvalError,Chrome 扩展和很多有安全要求的站点默认就是这个配置。

除此之外还要留意性能:解析字符串本身有成本,引擎也难对它做优化。所以真正合适的场景很窄,只有函数体在写代码时确实无法确定、且来源可信时才用它。绝大多数「动态」需求用高阶函数、查表或者策略对象就能解决,比如把若干实现放进一个 map 里按 key 取用,比拼字符串既安全又好调试。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEJavaScript new Function 构造函数与作用域加载中