eval 这个函数前端同学应该都见过,却很少使用过,甚至因为其存在一定的安全隐患而在很多编码规范中被禁止使用。笔者最近在看一些开源框架源码的时候,发现 eval 因其一个特点在一些场景中起到了非常关键的作用。因此,本文对 eval 的这一特点进行介绍。

首先我们来 eval 函数的定义:

The eval() function evaluates JavaScript code represented as a string and returns its completion value. The source is parsed as a script.

意思是说,eval 函数会将传给它的字符串作为 JavaScript 代码来执行,并将执行完成时的值作为返回值返回。比如 eval('1 + 1') 返回 2

根据规范中的定义,eval 函数在执行代码的时候,代码的作用域与调用 eval 的作用域有关。比如在下面的代码,最终获取的结果是内部的 x 的值。

 var x = 'outer';
  (function() {
    var x = 'inner';
    eval('x'); // "inner"
  })();
1
2
3
4
5

那么,这个看着平平无奇,而且使用不当还有一丝安全风险的 eval 到底有什么特点呢?

直接调用

所谓直接调用,规范中是这么定义的:

A direct call to the eval function is one that is expressed as a CallExpression that meets the following two conditions: The Reference that is the result of evaluating the MemberExpression in the CallExpression has an environment record as its base value and its reference name is "eval". The result of calling the abstract operation GetValue with that Reference as the argument is the standard builtin function defined in 15.1.2.1.

这里有两个名词需要先解释下,CallExpressionMemberExpression,这是 JavaScript 语法中的两种表达式类型。这里为了简便起见,我们可以这么理解,CallExpression 可以理解成函数调用表达式,而 MemberExpression 就是括号左边的表达式。详细说明可以参考规范open in new window中的定义。

现在我们来看直接调用的定义。所谓直接调用,就是同时满足如下两个条件的函数调用表达式:

  • 括号左边的表达式的结果是一个引用,同时这个引用的值是内置原生的 "eval" 函数
  • 这个应用的名称是 "eval"

注意,这里“引用”两个字被加粗了,这是判断是否是直接调用的关键,后面在间接调用的说明中我们会详细说明。

下面这些都是直接调用。

eval('...')
(eval)('...')
(((eval)))('...')
(function() { return eval('...') })()
eval('eval("...")')
(function(eval) { return eval('...'); })(eval)
with({ eval: eval }) eval('...')
with(window) eval('...')
1
2
3
4
5
6
7
8

直接调用的时候,代码的作用域与调用所在的作用域有关。这是符合规范定义的。

间接调用

简单来说,不是直接调用的都是间接调用。

如下的例子就都是间接调用。

const func = eval;
func('1 + 1');

(0, eval)(...)

eval = (function(eval) {
  return function(expr) {
    return eval(expr);
  };
})(eval);
eval('1+1');
1
2
3
4
5
6
7
8
9
10
11

眼尖的同学可能会问,为什么 (0, eval)(...) 是间接引用,而(eval)(...) 是直接调用呢?这就要回到被加粗的“引用”二字上来。

规范中定义,左侧表达式的结果必须是引用,不能是值。(0, eval)(...) 左边是逗号表达式,最终要返回一个值,即 eval 的值。(eval)(...) 左侧是括号(分组)表达式,不求值,因此结果还是引用。同理,(((eval)))(...) 依然是直接引用。

间接调用,执行代码的作用域是全局作用域。

微前端中的使用

讨论了直接调用和间接调用,我们知道,间接调用的时候,执行代码的作用域是全局作用域。这在微前端场景下就起到了非常关键的作用。

微前端的主应用需要将子应用运行起来,就需要执行子应用的代码。为了避免主应用代码的作用域影响到子应用的运行,需要保证子应用的代码是在全局作用域运行的。这就需要使用到 eval 的间接调用的特性。

同时为了做好沙箱隔离,通常在微前端中会借助各种方式来对 window 等全局变量做一个保护,比如通过 proxy 来处理。

下面是一段运行子应用的伪代码,简单示意一下处理流程。

const scriptText = fetch('https://path/to/subapp/code.js'); // 子应用代码

const globalWindow = (0, eval)('window'); // 获取全局的 window 对象
globalWindow.proxy = proxy; // 此处的 proxy 是经过处理后的沙箱。

// 将沙箱作为子应用的 window 来使用
const wrappedCode = `;(function(window, self, globalThis){;${scriptText}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`

(0, eval)(wrappedCode); // 执行代码
1
2
3
4
5
6
7
8
9

笔者查看了目前主流的几个微前端框架,不管是 qiankunopen in new window 还是 garfishopen in new window,运行子应用代码都是通过 eval 的间接调用来完成的。

关注微信公众号,获取最新推送~

加微信,深入交流~