动态执行脚本

提到动态执行脚本,大家想到的肯定是 evalnew 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动态地编译和执行代码,而代码的执行上下文是与当前进程隔离的,但是这里的隔离并不是绝对的安全,不完全等同浏览器的沙箱环境。

  1. 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 }
    
  2. 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 }
    
  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')

  4. eval()

    Nodejs 中同样可以使用 eval 函数,但性能和安全性有差异。请查看 https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/

  5. vm2

    Node.js 的高级 vm/sandbox,https://github.com/patriksimek/vm2

上下文隔离化

所有用 Node.js 所运行的 JavaScript 代码都是在一个“上下文”的作用域中被执行的。在 V8 中,一个上下文是一个执行环境,它允许分离的,无关的 JavaScript 应用在一个 V8 的单例中被运行。 必须明确地指定用于运行所有 JavaScript 代码的上下文。

vm.createContext([contextObject[, options]])

contextObject参数(如果 contextObjectundefined,则为新创建的对象)在内部与 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

你可能感兴趣的:(NodeJS)