提到动态执行脚本,大家想到的肯定是 eval
或 new Function()
,在 nodejs 中有专属的 vm 模块,可以完成相应的 sandbox 作用。
浏览器中动态执行脚本
eval()
函数会将传入的字符串当做 JavaScript 代码进行执行,返回字符串中代码的返回值;如果参数不是字符串将原封不动返回。
如果你间接的使用 eval()
,比如通过一个引用来调用它,而不是直接的调用 eval
。从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。
function test() {
let x = 2, y = 4;
console.log(eval('x + y')); // 直接调用,使用本地作用域,结果是 6
let geval = eval; // 等价于在全局作用域调用
console.log(geval('x + y')); // 间接调用,使用全局作用域,throws ReferenceError 因为`x`未定义
(0, eval)('x + y'); // 另一个间接调用的例子
}
eval
中函数作为字符串被定义需要“(”和“)”作为前缀和后缀
let fctStr1 = 'function a() {}'
let fctStr2 = '(function a() {})'
let fct1 = eval(fctStr1) // 返回undefined
let fct2 = eval(fctStr2) // 返回一个函数
MDN 建议永远不要使用 eval
-
eval()
使用与调用者相同的权限执行代码。如果你用eval()
运行的字符串代码被恶意方(不怀好意的人)修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码。更重要的是,第三方代码可以看到某一个eval()
被调用时的作用域,这也有可能导致一些不同方式的攻击。 -
eval()
通常比其他替代方法更慢,因为它必须调用 JS 解释器,而许多其他结构则可被现代 JS 引擎进行优化。此外,现代JavaScript解释器将javascript转换为机器代码。 这意味着任何变量命名的概念都会被删除。 因此,任意一个eval的使用都会强制浏览器进行冗长的变量名称查找,以确定变量在机器代码中的位置并设置其值。
Function 是替代 eval 的一个好的方法。
Function
new Function ([arg1[, arg2[, ...argN]],] functionBody)
每个 JavaScript 函数实际上都是一个 Function
对象。运行 (function(){}).constructor === Function // true
便可以得到这个结论。
与 eval
不同的是,Function
创建的函数只能在全局作用域中运行。
function test() {
let x = 2, y = 4;
console.log(new Function('return x + y')()); // 直接调用,使用全局作用域,throws ReferenceError
}
Nodejs 动态执行脚本
通过 node 的核心模块 vm 来实现。vm可以使用v8的Virtual Machine contexts动态地编译和执行代码,而代码的执行上下文是与当前进程隔离的,但是这里的隔离并不是绝对的安全,不完全等同浏览器的沙箱环境。
-
vm.runInContext(code, contextifiedObject[, options])
在指定的
contextifiedObject
的上下文里执行它并返回其结果。 被执行的代码无法获取本地作用域。contextifiedObject
必须是事先被vm.createContext()
方法上下文隔离化过的对象。const vm = require('vm') const contextObject = { a: 1 } vm.createContext(contextObject) const result = vm.runInContext('a += 1; b = 3', contextObject) console.log(result) // 3 { a: 2, b: 3 }
-
vm.runInNewContext(code[, contextObject[, options]])
给指定的
contextObject
(若为undefined
,则会新建一个contextObject
)提供一个隔离的上下文, 再在此上下文中执行编译的code
,最后返回结果。 运行中的代码无法获取本地作用域。const vm = require('vm') const result = vm.runInNewContext('a += 1; b = 3', {a: 1}) console.log(result) // 3 { a: 2, b: 3 }
-
vm.runInThisContext(code[, options])
在当前的
global
对象的上下文中编译并执行code
,最后返回结果。 运行中的代码无法获取本地作用域,但可以获取当前的global
对象。global.a = 1 const result = vm.runInThisContext('a += 1') console.log(result)
vm.runInThisContext()
更像是间接的执行eval()
, 就像(0,eval)('code')
。 -
eval()
Nodejs 中同样可以使用 eval 函数,但性能和安全性有差异。请查看 https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/
-
vm2
Node.js 的高级 vm/sandbox,https://github.com/patriksimek/vm2
上下文隔离化
所有用 Node.js 所运行的 JavaScript 代码都是在一个“上下文”的作用域中被执行的。在 V8 中,一个上下文是一个执行环境,它允许分离的,无关的 JavaScript 应用在一个 V8 的单例中被运行。 必须明确地指定用于运行所有 JavaScript 代码的上下文。
vm.createContext([contextObject[, options]])
contextObject
参数(如果 contextObject
为 undefined
,则为新创建的对象)在内部与 V8 上下文的新实例相关联。 该 V8 上下文提供了使用 vm
模块的方法运行的 code
以及可在其中运行的隔离的全局环境。
使用场景
动态执行字符串代码。vue ssr 中是通过 runInNewContext 实现的( Vue SSR 指南)。
参考地址
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
- http://nodejs.cn/api/vm.html
- https://ssr.vuejs.org/zh/api/#runinnewcontext
- https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/
- https://github.com/patriksimek/vm2