new Function + with:动态表达式求值的经典模式与现代演进

引子

在许多现代化前端脚手架(如旧版 vue-cli 的模板过滤机制、metalsmith 插件系统)中,我们经常会看到如下的代码结构:

module.exports = function evaluate(exp, data) {
  // 核心:动态构造一个执行函数
  const fn = new Function("data", "with (data) { return " + exp + "}");
  try {
    return fn(data);
  } catch (e) {
    console.error("过滤条件求值失败: " + exp);
  }
};

这段代码虽短,却浓缩了 JavaScript 的两个底层特性——new Functionwith。它是前端历史上用最小成本实现配置与逻辑解耦的经典奇技淫巧。本文将从原理、场景、陷阱、演进四个维度做深度剖析。


目录

  1. 为什么必须用 new Function
  2. with 关键字扮演了什么角色?
  3. 为什么现代开发严禁使用 with
  4. 何时仍可合理使用?
  5. 现代演进与替代方案
  6. 总结

一、 为什么必须用 new Function

使用 new Function 的核心目的是为了动态执行字符串表达式——将"文本"转化为"逻辑"。

1. 业务场景:动态流转的配置

在脚手架遍历模板文件时,配置文件(如 meta.json)通常会定义哪些文件在什么条件下需要被保留或过滤。例如:

  • 配置文件中定义的表达式(字符串):"prompts.router === true"
  • 用户选择的数据(对象):{ prompts: { router: true } }

如果直接写 if (exp),JavaScript 只会判断该文本是否为空字符串(true),永远无法阻拦文件。必须有一个机制,能把字符串 "prompts.router === true" 在运行时编译为真正的 JavaScript 表达式

new Function(arg, body) 正是为此而生——它能在运行期间临时组装并编译一段全新的 JavaScript 函数。

2. 为什么不用 eval()?(作用域隔离)

虽然 eval(exp) 也能执行字符串,但在动态执行脚本时,new Function 具备更优的作用域隔离(Scope Isolation)特性:

特性 eval() new Function
作用域访问 可读写当前函数的所有局部变量 只能访问全局作用域
副作用风险 高——可能意外修改闭包中的变量 低——无法触及外层局部变量
安全性 相对较高
function demo() {
  const secret = "should-not-leak";

  eval("console.log(secret)"); // ✅ 可以访问到 "should-not-leak"

  const fn = new Function("console.log(secret)");
  fn(); // ❌ ReferenceError: secret is not defined
}

核心差异new Function 创建的函数只绑定到全局作用域,无法访问当前函数的局部变量——这天然提供了比 eval 更好的隔离性。

3. CSP 限制

需要补充一个现代前端开发者必须知道的约束:

如果站点设置了严格的 Content-Security-Policy 且未包含 'unsafe-eval'new Functioneval被浏览器直接阻止。这也是它们逐渐被淘汰的一个现实原因——CSP 合规要求。


二、 with 关键字扮演了什么角色?

with 是 JavaScript 中的一个古老关键字(ES3 时代即存在)。它的核心作用是:临时改变作用域链,允许直接访问一个对象内部的属性,而无需重复编写该对象的名称。

1. 基础语法对比

const user = { name: "码农权", age: 18 };

// 正常写法
console.log(user.name); // "码农权"

// 使用 with
with (user) {
  console.log(name); // "码农权" —— 自动映射为 user.name
}

2. 在脚手架中的"魔法"作用

回到文章开头的代码:with (data) { return " + exp + " }

假设:

  • exp = "router"
  • data = { router: true, ts: true }
场景 写法 结果
不加 with data.router 或全局变量 router 报错 ReferenceError,找不到变量
with(data) 直接写 router ✅ 自动映射为 data.router,返回 true

with(data) 包裹在 new Function 的函数体内,使得模板配置文件中的表达式极大地简化:

// 用户在 meta.json 中只需要写
"condition": "router && ts"

// 而不是
"condition": "data.router === true && data.ts === true"

这本质上是一种语法糖注入——把 data 对象的属性提升到当前作用域的变量查找优先级中。


三、 为什么现代开发严禁使用 with

尽管 with 在脚手架和模板引擎中极其方便,但在日常业务代码中它被明令禁止——在严格模式 "use strict" 下使用会直接抛出 SyntaxError。原因有二。

1. 语义模糊,极易引发不可预测的 Bug

function updateWidget(obj) {
  with (obj) {
    width = 100; // 这条赋值语句的目标是谁??
  }
}
传入的 obj 行为 后果
{ width: 50 } 修改 obj.width = 100 ✅ 符合预期
{ height: 50 }(无 width with 沿作用域链向上查找 width ⚠️ 若外层也没有,隐式创建全局变量 window.width = 100

这个不确定性是大型工程的灾难——一段代码的行为完全取决于运行时传入的对象的形状,代码审查和静态分析都无法有效覆盖。

2. 破坏 V8 引擎的性能优化

现代 JavaScript 引擎(V8、SpiderMonkey 等)在编译阶段需要确定每个变量在内存中的物理位置,以进行激进的机器码优化(如内联缓存 Inline Cache)。

一旦遭遇 with

┌─────────────────────────────────────┐
│          正常代码编译流程              │
│                                     │
│  源码 → AST → 确定变量位置 → 优化机器码 │
│         (编译时即可确定)              │
└─────────────────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│        with 出现后的编译流程           │
│                                     │
│  源码 → AST → ❓ 变量属于谁 ❓         │
│          (编译时无法确定)             │
│               ↓                      │
│        放弃优化 → 回退到解释执行        │
└─────────────────────────────────────┘

引擎在运行前根本无法预测大括号内部的变量到底属于谁——必须等待运行时根据传入的对象才能决定。这会直接导致引擎放弃对该段代码的所有编译优化,性能出现断崖式下跌。

简言之with 让引擎从"信心的巅峰"跌落到"完全无法优化",相当于要求引擎必须每次去翻一遍作用域链字典。


四、 何时仍可合理使用?

虽然 with 在业务代码中已被弃用,但在特定受限场景下,new Function + with 的组合仍然是一个务实的选择:

场景 是否推荐 原因
CLI 工具 / 脚手架(信任输入) ✅ 仍可接受 输入受控、不暴露给不可信用户、无需 CSP 合规
Node.js 构建脚本 ✅ 仍可接受 无 CSP 风险,输入来自本地配置文件
浏览器端、用户可输入表达式 ❌ 禁止 CSP 风险 + 安全风险,应替换为 AST 解析器
公开库 / 组件库 ❌ 不建议 使用者可能受 CSP 限制,或运行在严格模式下

五、 现代演进与替代方案

随着前端工具链的发展,new Function + with 正在被以下方案逐步取代:

1. AST 静态编译(最推荐)

// 使用 @babel/parser 解析表达式为 AST
// 然后遍历 AST 替换变量引用为 data.xxx
// 最终生成安全的编译后代码

代表工具:Vite 模板、create-vue@rollup/pluginutils

优点:无 eval、无 CSP 问题、可静态分析、无性能惩罚。

2. 解构赋值 + 预编译

function evaluate(exp, data) {
  const keys = Object.keys(data);
  const values = Object.values(data);
  const fn = new Function(...keys, "return " + exp);
  return fn(...values);
}

with 被解构参数取代:传入 { router: true } 时,keys = ['router'],函数签名变为 function evaluate(router) { return router }无需 with,也无性能惩罚。

3. 专用表达式求值库

特点 适用场景
expr-eval 纯数学表达式 + 变量求值 公式计算、条件判断
jiti 运行时加载 JS/TS 模块 Node.js 动态导入
isolated-vm 独立 V8 隔离实例 高安全性要求的沙箱执行

注意vm2 已正式退役(2023 年因无法彻底修复沙箱逃逸漏洞而被原作者存档),不推荐新项目使用。

4. 各方案对比总表

方案 动态执行 安全性 性能 CSP 合规 实现成本
new Function + with ❌ 低 ❌ 差(引擎放弃优化) ❌ 需 unsafe-eval 极低
new Function + 解构 ⚠️ 中 ✅ 正常 ❌ 需 unsafe-eval
AST 静态编译 ✅ 高 ✅ 正常 ✅ 合规
专用表达式库 ⚠️ 中~高 ✅ 正常 视实现而定

六、 总结

┌──────────────────────────────────────────────────┐
│           动态表达式求值的演进路线                   │
│                                                    │
│  new Function + with(经典但不安全)                 │
│          │                                         │
│          ▼                                         │
│  new Function + 解构参数(无 with,性能回归)        │
│          │                                         │
│          ▼                                         │
│  AST 静态编译 / 沙箱库(现代工程的最佳实践)          │
└──────────────────────────────────────────────────┘
  • new Function + with 是前端历史上用极少的代码满足配置与逻辑解耦需求的经典组合。理解它,有助于读懂大量中古前端项目的源码。
  • 理解其原理而非沿用其写法——知道它解决了什么问题、为什么能在那个时代存在、以及为什么今天应该用更好的方案,才是一个工程师真正的收获。

技术选型的本质,是在什么时候用什么东西,而不是在什么东西最好用
new Function + with 在过去是巧妙的,在今天是被淘汰的,但理解了它,你就理解了 JavaScript 语言设计中一段不可忽略的历史。

posted @ 2026-06-29 17:23  HuangBingQuan  阅读(9)  评论(0)    收藏  举报