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 Function 和 with。它是前端历史上用最小成本实现配置与逻辑解耦的经典奇技淫巧。本文将从原理、场景、陷阱、演进四个维度做深度剖析。
目录
一、 为什么必须用 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 Function和eval会被浏览器直接阻止。这也是它们逐渐被淘汰的一个现实原因——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 语言设计中一段不可忽略的历史。

浙公网安备 33010602011771号