w3ctech

eval 的递归: 保留本地 eval 上下文

标题可能有些让人困惑, 在进入正文之前先举一个简单的例子:

function __eval(expr) {
    return eval(expr);
}

__eval('var foo = 123;');
__eval('foo;'); // ReferenceError.

前一段时间做了一个远程控制台, 是公司 JavaScript 智能硬件平台 Ruff 某个项目的一个功能. 这个控制台在执行输入的表达式时有以下要求:

  1. 它需要在一个被立即执行的函数内执行表达式. 和 Node.js 类似, 每个模块在编译时被放在了一个被立即执行的函数内. 开发者可能会在模块内直接定义变量和函数, 如果是应用的入口模块, 我们期望控制台能访问到该模块内的变量及函数.
  2. 它需要能访问之前被执行的语句创建的变量和函数. 这个很好理解, 我们希望在控制台输入表达式时能像 REPL 一样保留上下文.

我编写了一个简单地 Babel 插件, 访问 (visit) VariableDeclarationFunctionDeclaration 两种节点并将其转换为对 global 对象属性的赋值语句. 比如 var abc = 123; 会被转换为 global.abc = 123; undefined;. 当然, 具体的转换会有一些细节处理, 使行为尽可能与预期一致.

不过最近几天, 一个同事突然对这个东西好奇起来并且觉得可能可以在不进行表达式转换的情况下实现对上下文的保留. 我一开始对这个想法的态度是: 妈蛋我对 JS 多了解? 说不行就不行. 不过在出门吃午饭的路上我意识到自己可能错了, 交换想法后我们各自拿出了一套实现.

以下是我的版本 (精简后):

var __eval = function () {
    __eval = eval(`(${arguments.callee.toString()})`);
    return eval(arguments[0]);
};

背后的点在于执行一个函数, 这个函数含有会 "递归" 执行它本身对应的表达式的 eval. 可能这样有点难以理解, 接下来我们尝试展开对于函数的 eval:

var __eval = function () {
    __eval = function () {
        __eval = eval(`(${arguments.callee.toString()})`);
        return eval(arguments[0]);
    };
    return eval(arguments[0]);
};

再一层:

var __eval = function () {
    __eval = function () {
        __eval = function () {
            __eval = eval(`(${arguments.callee.toString()})`);
            return eval(arguments[0]);
        };
        return eval(arguments[0]);
    };
    return eval(arguments[0]);
};

所以每当 __eval 被调用时, 它会在上一次 __eval 创建的函数中再次创建新的 __eval 函数, 如此上下文就得到了保留!

然而这样做还是有其局限性. 一方面这使得整个链无法被释放, 另一方面新表达式中的声明会 "屏蔽" 掉之前执行的表达式中的声明. 然而递归 eval 的想法本身还是很有意思, 吧?


关于作者

GitHub https://github.com/vilic
知乎 https://www.zhihu.com/people/vilicvane

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复