Skip to content

VM(执行 JavaScript)

[稳定: 2 - 稳定]

稳定: 2 稳定性: 2 - 稳定

源代码: lib/vm.js

node:vm 模块允许在 V8 虚拟机上下文中编译和运行代码。

node:vm 模块并非安全机制。请勿使用它运行不受信任的代码。

JavaScript 代码可以立即编译和运行,也可以编译、保存后再运行。

一个常见的用例是在不同的 V8 上下文中运行代码。这意味着被调用的代码具有与调用代码不同的全局对象。

可以通过 上下文化 对象来提供上下文。被调用的代码将上下文中的任何属性视为全局变量。被调用代码对全局变量所做的任何更改都会反映在上下文对象中。

js
const vm = require('node:vm')

const x = 1

const context = { x: 2 }
vm.createContext(context) // 上下文化对象。

const code = 'x += 40; var y = 17;'
// `x` 和 `y` 是上下文中的全局变量。
// 最初,x 的值为 2,因为这是 context.x 的值。
vm.runInContext(code, context)

console.log(context.x) // 42
console.log(context.y) // 17

console.log(x) // 1; y 未定义。

类: vm.Script

新增于: v0.3.1

vm.Script 类的实例包含预编译的脚本,可以在特定的上下文中执行。

new vm.Script(code[, options])

[历史记录]

版本变更
v21.7.0, v20.12.0添加了对 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 的支持。
v17.0.0, v16.12.0importModuleDynamically 参数添加了对导入属性的支持。
v10.6.0produceCachedData 已弃用,建议使用 script.createCachedData()
v5.7.0现在支持 cachedDataproduceCachedData 选项。
v0.3.1新增于: v0.3.1
  • code <字符串> 要编译的 JavaScript 代码。
  • options <对象> | <字符串>
    • filename <字符串> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.\<anonymous\>'
    • lineOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供一个可选的 BufferTypedArray,或包含 V8 代码缓存数据的 DataView。提供此数据后,cachedDataRejected 值将设置为 truefalse,具体取决于 V8 是否接受该数据。
    • produceCachedData <布尔值> 当值为 true 且不存在 cachedData 时,V8 将尝试为 code 生成代码缓存数据。成功后,将生成包含 V8 代码缓存数据的 Buffer 并将其存储在返回的 vm.Script 实例的 cachedData 属性中。cachedDataProduced 值将设置为 truefalse,具体取决于是否成功生成代码缓存数据。此选项已弃用,建议使用 script.createCachedData()默认值: false
    • importModuleDynamically <函数> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在调用 import() 时如何加载此脚本评估期间的模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。有关详细信息,请参阅编译 API 中动态 import() 的支持

如果 options 是一个字符串,则它指定文件名。

创建新的 vm.Script 对象会编译 code,但不会运行它。编译后的 vm.Script 可以稍后多次运行。code 不绑定到任何全局对象;而是在每次运行之前绑定,仅用于该次运行。

script.cachedDataRejected

新增于: v5.7.0

当使用 cachedData 创建 vm.Script 时,此值将根据 V8 是否接受数据而设置为 truefalse。否则值为 undefined

script.createCachedData()

新增于: v10.6.0

创建一个可用于 Script 构造函数的 cachedData 选项的代码缓存。返回一个 Buffer。此方法可以在任何时间和任何次数调用。

Script 的代码缓存不包含任何 JavaScript 可观察状态。代码缓存可以安全地与脚本源一起保存,并用于多次构造新的 Script 实例。

Script 源中的函数可以标记为延迟编译,它们不会在 Script 构造时编译。这些函数将在第一次调用时编译。代码缓存序列化 V8 当前知道的关于 Script 的元数据,V8 可以使用这些元数据来加快未来的编译速度。

js
const script = new vm.Script(`
function add(a, b) {
  return a + b;
}

const x = add(1, 2);
`)

const cacheWithoutAdd = script.createCachedData()
// 在 `cacheWithoutAdd` 中,函数 `add()` 被标记为在调用时进行完整编译。

script.runInThisContext()

const cacheWithAdd = script.createCachedData()
// `cacheWithAdd` 包含已完全编译的函数 `add()`。

script.runInContext(contextifiedObject[, options])

[历史]

版本变更
v6.3.0现在支持 breakOnSigint 选项。
v0.3.1v0.3.1 版本新增
  • contextifiedObject <对象>vm.createContext() 方法返回的 上下文化 对象。

  • options <对象>

    • displayErrors <布尔值> 当值为 true 时,如果在编译 code 时发生 Error 错误,则导致错误的代码行将附加到堆栈跟踪中。默认值: true
    • timeout <整数> 指定在终止执行之前执行 code 的毫秒数。如果执行终止,则会抛出 Error 错误。此值必须是严格的正整数。
    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出 Error 错误。通过 process.on('SIGINT') 附加的事件现有处理程序在脚本执行期间被禁用,但之后继续工作。默认值: false
  • 返回值: <任意值> 脚本中最后执行语句的结果。

在给定的 contextifiedObject 中运行 vm.Script 对象包含的已编译代码,并返回结果。运行代码无法访问局部作用域。

以下示例编译递增全局变量、设置另一个全局变量的值,然后多次执行代码的代码。全局变量包含在 context 对象中。

js
const vm = require('node:vm')

const context = {
  animal: 'cat',
  count: 2,
}

const script = new vm.Script('count += 1; name = "kitty";')

vm.createContext(context)
for (let i = 0; i < 10; ++i) {
  script.runInContext(context)
}

console.log(context)
// 打印:{ animal: 'cat', count: 12, name: 'kitty' }

使用 timeoutbreakOnSigint 选项将导致启动新的事件循环和相应的线程,这些线程具有非零的性能开销。

script.runInNewContext([contextObject[, options]])

[历史]

版本变更
v22.8.0, v20.18.0contextObject 参数现在接受 vm.constants.DONT_CONTEXTIFY
v14.6.0现在支持 microtaskMode 选项。
v10.0.0现在支持 contextCodeGeneration 选项。
v6.3.0现在支持 breakOnSigint 选项。
v0.3.1v0.3.1 版本中添加
  • contextObject <对象> | <vm.constants.DONT_CONTEXTIFY> | <未定义> vm.constants.DONT_CONTEXTIFY 或将被 上下文化 的对象。如果为 undefined,则为了向后兼容性将创建一个空的上下文化对象。

  • options <对象>

    • displayErrors <布尔值> 如果为 true,则如果在编译 code 时发生 Error 错误,则导致错误的代码行将附加到堆栈跟踪中。默认值: true

    • timeout <整数> 指定在终止执行之前执行 code 的毫秒数。如果执行被终止,则将抛出 Error 错误。此值必须是严格的正整数。

    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出 Error 错误。在脚本执行期间,通过 process.on('SIGINT') 附加的事件的现有处理程序将被禁用,但在之后继续工作。默认值: false

    • contextName <字符串> 新创建上下文的易于理解的名称。默认值: 'VM Context i',其中 i 是已创建上下文的递增数字索引。

    • contextOrigin <字符串> 用于显示目的的与新创建上下文相对应的 来源。来源应格式化为 URL,但仅包含方案、主机和端口(如有必要),类似于 URL 对象的 url.origin 属性的值。最值得注意的是,此字符串应省略尾部斜杠,因为那表示路径。默认值: ''

    • contextCodeGeneration <对象>

    • strings <布尔值> 如果设置为 false,则对 eval 或函数构造函数(FunctionGeneratorFunction 等)的任何调用都将抛出 EvalError默认值: true

    • wasm <布尔值> 如果设置为 false,则任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true

    • microtaskMode <字符串> 如果设置为 afterEvaluate,则微任务(通过 Promiseasync function 调度的任务)将在脚本运行后立即运行。在这种情况下,它们包含在 timeoutbreakOnSigint 范围内。

  • 返回值: <任意值> 脚本中执行的最后一个语句的结果。

此方法是 script.runInContext(vm.createContext(options), options) 的快捷方式。它同时执行几件事:

以下示例编译设置全局变量的代码,然后在不同上下文中多次执行该代码。全局变量在每个单独的 context 中设置和包含。

js
const vm = require('node:vm')

const script = new vm.Script('globalVar = "set"')

const contexts = [{}, {}, {}]
contexts.forEach(context => {
  script.runInNewContext(context)
})

console.log(contexts)
// 打印:[{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]

// 如果上下文是从上下文化对象创建的,则这将抛出错误。
// vm.constants.DONT_CONTEXTIFY 允许使用可以冻结的普通全局对象创建上下文。
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;')
const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY)

script.runInThisContext([options])

[历史]

版本变更
v6.3.0现在支持 breakOnSigint 选项。
v0.3.1v0.3.1 版本新增
  • options <对象>

    • displayErrors <布尔值> 当值为 true 时,如果在编译 code 时发生 Error 错误,则导致错误的代码行将附加到堆栈跟踪中。默认值: true
    • timeout <整数> 指定在终止执行之前执行 code 的毫秒数。如果执行被终止,则会抛出 Error 错误。此值必须是严格的正整数。
    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出 Error 错误。通过 process.on('SIGINT') 附加的事件现有处理程序在脚本执行期间被禁用,但在之后继续工作。默认值: false
  • 返回值: <任意值> 脚本中最后执行语句的结果。

在当前 global 对象的上下文中运行 vm.Script 包含的已编译代码。运行的代码无法访问局部作用域,但可以访问当前的 global 对象。

以下示例编译递增 global 变量的代码,然后多次执行该代码:

js
const vm = require('node:vm')

global.globalVar = 0

const script = new vm.Script('globalVar += 1', { filename: 'myfile.vm' })

for (let i = 0; i < 1000; ++i) {
  script.runInThisContext()
}

console.log(globalVar)

// 1000

script.sourceMapURL

新增于:v19.1.0, v18.13.0

当脚本编译自包含源映射魔法注释的源代码时,此属性将设置为源映射的 URL。

js
import vm from 'node:vm'

const script = new vm.Script(`
function myFunc() {}
//# sourceMappingURL=sourcemap.json
`)

console.log(script.sourceMapURL)
// 输出:sourcemap.json
js
const vm = require('node:vm')

const script = new vm.Script(`
function myFunc() {}
//# sourceMappingURL=sourcemap.json
`)

console.log(script.sourceMapURL)
// 输出:sourcemap.json

类:vm.Module

新增于:v13.0.0, v12.16.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1 - 实验性

此功能仅在启用 --experimental-vm-modules 命令标志时可用。

vm.Module 类提供了一个低级接口,用于在 VM 上下文中使用 ECMAScript 模块。它是 vm.Script 类的对应类,密切反映了 ECMAScript 规范中定义的 模块记录

然而,与 vm.Script 不同的是,每个 vm.Module 对象都从其创建时就绑定到一个上下文。与 vm.Script 对象的同步特性相反,vm.Module 对象上的操作本质上是异步的。使用 'async' 函数可以帮助操作 vm.Module 对象。

使用 vm.Module 对象需要三个不同的步骤:创建/解析、链接和评估。以下示例说明了这三个步骤。

此实现位于比 ECMAScript 模块加载器 更低的级别。目前还没有办法与加载器交互,尽管计划支持。

js
import vm from 'node:vm'

const contextifiedObject = vm.createContext({
  secret: 42,
  print: console.log,
})

// 步骤 1
//
// 通过构造一个新的 `vm.SourceTextModule` 对象来创建一个模块。这将解析提供的源文本,如果出现任何错误,则抛出 `SyntaxError`。默认情况下,模块是在顶级上下文中创建的。但是在这里,我们将 `contextifiedObject` 指定为此模块所属的上下文。
//
// 在这里,我们尝试从模块 "foo" 获取默认导出,并将其放入局部绑定 "secret"。

const bar = new vm.SourceTextModule(
  `
  import s from 'foo';
  s;
  print(s);
`,
  { context: contextifiedObject }
)

// 步骤 2
//
// 将此模块的导入依赖项“链接”到它。
//
// 提供的链接回调(“链接器”)接受两个参数:父模块(在本例中为 `bar`)和导入模块的说明符字符串。回调预计将返回一个对应于提供的说明符的模块,其某些要求在 `module.link()` 中有记录。
//
// 如果返回的模块尚未开始链接,则将对返回的模块调用相同的链接器回调。
//
// 即使是没有任何依赖项的顶级模块也必须显式链接。但是,提供的回调将永远不会被调用。
//
// link() 方法返回一个 Promise,当链接器返回的所有 Promise 都解析后,该 Promise 将被解析。
//
// 注意:这是一个人为的示例,因为链接器函数每次被调用时都会创建一个新的 "foo" 模块。在一个成熟的模块系统中,可能会使用缓存来避免重复的模块。

async function linker(specifier, referencingModule) {
  if (specifier === 'foo') {
    return new vm.SourceTextModule(
      `
      // "secret" 变量引用我们在创建上下文时添加到 "contextifiedObject" 的全局变量。
      export default secret;
    `,
      { context: referencingModule.context }
    )

    // 在这里使用 `contextifiedObject` 代替 `referencingModule.context` 也同样有效。
  }
  throw new Error(`无法解析依赖项:${specifier}`)
}
await bar.link(linker)

// 步骤 3
//
// 评估模块。evaluate() 方法返回一个 promise,该 promise 将在模块评估完成后解析。

// 输出 42。
await bar.evaluate()
js
const vm = require('node:vm')

const contextifiedObject = vm.createContext({
  secret: 42,
  print: console.log,
})

;(async () => {
  // 步骤 1
  //
  // 通过构造一个新的 `vm.SourceTextModule` 对象来创建一个模块。这将解析提供的源文本,如果出现任何错误,则抛出 `SyntaxError`。默认情况下,模块是在顶级上下文中创建的。但是在这里,我们将 `contextifiedObject` 指定为此模块所属的上下文。
  //
  // 在这里,我们尝试从模块 "foo" 获取默认导出,并将其放入局部绑定 "secret"。

  const bar = new vm.SourceTextModule(
    `
    import s from 'foo';
    s;
    print(s);
  `,
    { context: contextifiedObject }
  )

  // 步骤 2
  //
  // 将此模块的导入依赖项“链接”到它。
  //
  // 提供的链接回调(“链接器”)接受两个参数:父模块(在本例中为 `bar`)和导入模块的说明符字符串。回调预计将返回一个对应于提供的说明符的模块,其某些要求在 `module.link()` 中有记录。
  //
  // 如果返回的模块尚未开始链接,则将对返回的模块调用相同的链接器回调。
  //
  // 即使是没有任何依赖项的顶级模块也必须显式链接。但是,提供的回调将永远不会被调用。
  //
  // link() 方法返回一个 Promise,当链接器返回的所有 Promise 都解析后,该 Promise 将被解析。
  //
  // 注意:这是一个人为的示例,因为链接器函数每次被调用时都会创建一个新的 "foo" 模块。在一个成熟的模块系统中,可能会使用缓存来避免重复的模块。

  async function linker(specifier, referencingModule) {
    if (specifier === 'foo') {
      return new vm.SourceTextModule(
        `
        // "secret" 变量引用我们在创建上下文时添加到 "contextifiedObject" 的全局变量。
        export default secret;
      `,
        { context: referencingModule.context }
      )

      // 在这里使用 `contextifiedObject` 代替 `referencingModule.context` 也同样有效。
    }
    throw new Error(`无法解析依赖项:${specifier}`)
  }
  await bar.link(linker)

  // 步骤 3
  //
  // 评估模块。evaluate() 方法返回一个 promise,该 promise 将在模块评估完成后解析。

  // 输出 42。
  await bar.evaluate()
})()

module.dependencySpecifiers

此模块所有依赖项的说明符。返回的数组是冻结的,不允许对其进行任何更改。

对应于 ECMAScript 规范中循环模块记录[[RequestedModules]]字段。

module.error

如果module.status'errored',则此属性包含模块在评估期间抛出的异常。如果状态为其他任何值,则访问此属性将导致抛出异常。

由于可能与throw undefined;混淆,因此undefined值不能用于没有抛出异常的情况。

对应于 ECMAScript 规范中循环模块记录[[EvaluationError]]字段。

module.evaluate([options])

  • options <对象>

    • timeout <整数> 指定在终止执行前评估的毫秒数。如果执行被中断,则会抛出一个 Error 错误。此值必须是严格正整数。
    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出一个 Error 错误。通过 process.on('SIGINT') 附加的事件现有处理程序在脚本执行期间被禁用,但在之后继续工作。默认值: false
  • 返回值: <Promise> 成功时以 undefined 作为结果完成。

评估模块。

这必须在模块链接后调用;否则它将被拒绝。当模块已经被评估时也可以调用它,在这种情况下,如果初始评估成功(module.status'evaluated'),它将什么也不做,或者它将重新抛出初始评估导致的异常(module.status'errored')。

在模块正在评估中时(module.status'evaluating'),无法调用此方法。

对应于 ECMAScript 规范中 循环模块记录Evaluate()具体方法 字段。

module.identifier

当前模块的标识符,在构造函数中设置。

module.link(linker)

[历史]

版本变更
v21.1.0, v20.10.0, v18.19.0extra.assert 选项重命名为 extra.attributes。为了向后兼容,仍然提供旧名称。

链接模块依赖项。此方法必须在评估之前调用,并且每个模块只能调用一次。

该函数应返回一个 Module 对象或一个最终解析为 Module 对象的 Promise。返回的 Module 必须满足以下两个不变式:

  • 它必须属于与父 Module 相同的上下文。
  • 它的 status 不能是 'errored'

如果返回的 Modulestatus'unlinked',则此方法将使用相同的提供的 linker 函数递归调用返回的 Module

link() 返回一个 Promise,当所有链接实例都解析为有效的 Module 时,它将被解析;如果链接器函数抛出异常或返回无效的 Module,则会被拒绝。

链接器函数大致对应于 ECMAScript 规范中实现定义的 HostResolveImportedModule 抽象操作,但有一些关键区别:

在模块链接期间使用的实际 HostResolveImportedModule 实现是返回在链接期间链接的模块的实现。因为那时所有模块都已完全链接,所以根据规范,HostResolveImportedModule 实现是完全同步的。

对应于 ECMAScript 规范中 循环模块记录Link() 具体方法 字段。

module.namespace

模块的命名空间对象。只有在链接 (module.link()) 完成后才能访问。

对应于 ECMAScript 规范中的 GetModuleNamespace 抽象操作。

module.status

模块的当前状态。将是以下之一:

  • 'unlinked': 尚未调用 module.link()
  • 'linking': 已调用 module.link(),但链接器函数返回的所有 Promise 尚未全部解析。
  • 'linked': 模块已成功链接,其所有依赖项都已链接,但尚未调用 module.evaluate()
  • 'evaluating': 模块正在通过自身或父模块上的 module.evaluate() 进行评估。
  • 'evaluated': 模块已成功评估。
  • 'errored': 模块已评估,但抛出了异常。

除了 'errored' 之外,此状态字符串对应于规范中 循环模块记录[[Status]] 字段。'errored' 对应于规范中的 'evaluated',但 [[EvaluationError]] 设置为非 undefined 的值。

类: vm.SourceTextModule

新增于: v9.6.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1 - 实验性

此特性仅在启用 --experimental-vm-modules 命令行标志时可用。

vm.SourceTextModule 类提供 ECMAScript 规范中定义的 源文本模块记录

new vm.SourceTextModule(code[, options])

[历史]

版本变更
v17.0.0, v16.12.0importModuleDynamically 参数添加了对导入属性的支持。
  • code <string> 要解析的 JavaScript 模块代码

  • options

    • identifier <string> 堆栈跟踪中使用的字符串。默认值: 'vm:module(i)',其中 i 是上下文相关的递增索引。

    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArrayDataView,其中包含 V8 的代码缓存数据,用于提供的源代码。code 必须与创建此 cachedData 的模块相同。

    • context <Object>vm.createContext() 方法返回的 上下文化 对象,用于编译和评估此 Module。如果未指定上下文,则模块在当前执行上下文中进行评估。

    • lineOffset <integer> 指定此 Module 生成的堆栈跟踪中显示的行号偏移量。默认值: 0

    • columnOffset <integer> 指定此 Module 生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0

    • initializeImportMeta <Function> 在评估此 Module 期间调用,以初始化 import.meta

    • meta <import.meta>

    • module <vm.SourceTextModule>

    • importModuleDynamically <Function> 用于指定调用 import() 时,在评估此模块期间如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。有关详细信息,请参阅 编译 API 中动态 import() 的支持

创建一个新的 SourceTextModule 实例。

分配给 import.meta 对象的属性如果是对象,则可能允许模块访问指定 context 之外的信息。使用 vm.runInContext() 在特定上下文中创建对象。

js
import vm from 'node:vm'

const contextifiedObject = vm.createContext({ secret: 42 })

const module = new vm.SourceTextModule('Object.getPrototypeOf(import.meta.prop).secret = secret;', {
  initializeImportMeta(meta) {
    // 注意:此对象是在顶级上下文中创建的。因此,
    // Object.getPrototypeOf(import.meta.prop) 指向顶级上下文中的
    // Object.prototype,而不是上下文化对象中的 Object.prototype。
    meta.prop = {}
  },
})
// 由于模块没有依赖项,因此链接器函数将永远不会被调用。
await module.link(() => {})
await module.evaluate()

// 现在,Object.prototype.secret 将等于 42。
//
// 要解决此问题,请将
//     meta.prop = {};
// 以上替换为
//     meta.prop = vm.runInContext('{}', contextifiedObject);
js
const vm = require('node:vm')
const contextifiedObject = vm.createContext({ secret: 42 })
;(async () => {
  const module = new vm.SourceTextModule('Object.getPrototypeOf(import.meta.prop).secret = secret;', {
    initializeImportMeta(meta) {
      // 注意:此对象是在顶级上下文中创建的。因此,
      // Object.getPrototypeOf(import.meta.prop) 指向顶级上下文中的
      // Object.prototype,而不是上下文化对象中的 Object.prototype。
      meta.prop = {}
    },
  })
  // 由于模块没有依赖项,因此链接器函数将永远不会被调用。
  await module.link(() => {})
  await module.evaluate()
  // 现在,Object.prototype.secret 将等于 42。
  //
  // 要解决此问题,请将
  //     meta.prop = {};
  // 以上替换为
  //     meta.prop = vm.runInContext('{}', contextifiedObject);
})()

sourceTextModule.createCachedData()

新增于:v13.7.0, v12.17.0

创建一个代码缓存,可用于 SourceTextModule 构造函数的 cachedData 选项。返回一个 Buffer。在模块被评估之前,此方法可以被调用任意多次。

SourceTextModule 的代码缓存不包含任何 JavaScript 可观察状态。代码缓存可以安全地与脚本源一起保存,并用于多次构造新的 SourceTextModule 实例。

SourceTextModule 源代码中的函数可以标记为延迟编译,它们不会在 SourceTextModule 构造时进行编译。这些函数将在第一次调用时进行编译。代码缓存会序列化 V8 当前知道的关于 SourceTextModule 的元数据,V8 可以利用这些元数据来加快未来的编译速度。

js
// 创建一个初始模块
const module = new vm.SourceTextModule('const a = 1;')

// 从此模块创建缓存数据
const cachedData = module.createCachedData()

// 使用缓存数据创建一个新模块。代码必须相同。
const module2 = new vm.SourceTextModule('const a = 1;', { cachedData })

类: vm.SyntheticModule

新增于: v13.0.0, v12.16.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1 - 实验性

此功能仅在启用 --experimental-vm-modules 命令行标志时可用。

vm.SyntheticModule 类提供了 WebIDL 规范中定义的 合成模块记录。合成模块的目的是提供一个通用接口,用于将非 JavaScript 源代码暴露给 ECMAScript 模块图。

js
const vm = require('node:vm')

const source = '{ "a": 1 }'
const module = new vm.SyntheticModule(['default'], function () {
  const obj = JSON.parse(source)
  this.setExport('default', obj)
})

// 在链接中使用 `module`...

new vm.SyntheticModule(exportNames, evaluateCallback[, options])

新增于: v13.0.0, v12.16.0

  • exportNames <string[]> 模块将导出的名称数组。
  • evaluateCallback <Function> 模块被评估时调用。
  • options
    • identifier <string> 用于堆栈跟踪的字符串。默认值: 'vm:module(i)',其中 i 是上下文相关的递增索引。
    • context <Object>vm.createContext() 方法返回的 上下文化 对象,用于编译和评估此 Module

创建新的 SyntheticModule 实例。

分配给此实例导出的对象可能允许模块的导入者访问指定 context 之外的信息。使用 vm.runInContext() 在特定上下文中创建对象。

syntheticModule.setExport(name, value)

新增于: v13.0.0, v12.16.0

  • name <string> 要设置的导出的名称。
  • value <any> 要设置为导出的值。

此方法用于在模块链接后设置导出的值。如果在模块链接之前调用它,则会抛出 ERR_VM_MODULE_STATUS 错误。

js
import vm from 'node:vm'

const m = new vm.SyntheticModule(['x'], () => {
  m.setExport('x', 1)
})

await m.link(() => {})
await m.evaluate()

assert.strictEqual(m.namespace.x, 1)
js
const vm = require('node:vm')
;(async () => {
  const m = new vm.SyntheticModule(['x'], () => {
    m.setExport('x', 1)
  })
  await m.link(() => {})
  await m.evaluate()
  assert.strictEqual(m.namespace.x, 1)
})()

vm.compileFunction(code[, params[, options]])

[历史]

版本变更
v21.7.0, v20.12.0新增对 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 的支持。
v19.6.0, v18.15.0如果传递了 cachedData 选项,则返回值现在包含 cachedDataRejected,其语义与 vm.Script 版本相同。
v17.0.0, v16.12.0importModuleDynamically 参数添加了对导入属性的支持。
v15.9.0再次添加 importModuleDynamically 选项。
v14.3.0由于兼容性问题,删除了 importModuleDynamically
v14.1.0, v13.14.0现在支持 importModuleDynamically 选项。
v10.10.0v10.10.0 版本中添加
  • code <字符串> 要编译的函数体。

  • params <字符串数组> 包含函数所有参数的字符串数组。

  • options <对象>

    • filename <字符串> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: ''
    • lineOffset <数字> 指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <数字> 指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArray,或带有 V8 代码缓存数据的 DataView。这必须由先前对 vm.compileFunction() 的调用使用相同的 codeparams 生成。
    • produceCachedData <布尔值> 指定是否生成新的缓存数据。默认值: false
    • parsingContext <对象> 该函数应在其内编译的 上下文化 对象。
    • contextExtensions <对象数组> 包含要应用于编译期间的上下文扩展(包装当前作用域的对象)集合的数组。默认值: []
  • importModuleDynamically <函数> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定调用 import() 时,在此函数的评估期间应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。有关详细信息,请参阅 编译 API 中动态 import() 的支持

  • 返回值: <函数>

将给定的代码编译到提供的上下文(如果未提供上下文,则使用当前上下文),并将其包装在具有给定 params 的函数中返回。

vm.constants

新增于:v21.7.0, v20.12.0

返回一个包含 VM 操作常用常量的对象。

vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER

新增于:v21.7.0, v20.12.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.1 - 活跃开发中

一个常量,可以用作 vm.Scriptvm.compileFunction()importModuleDynamically 选项,以便 Node.js 使用主上下文中的默认 ESM 加载器来加载请求的模块。

更多详细信息,请参阅 编译 API 中动态 import() 的支持

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

[历史]

版本变更
v22.8.0, v20.18.0contextObject 参数现在接受 vm.constants.DONT_CONTEXTIFY
v21.7.0, v20.12.0添加了对 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 的支持。
v21.2.0, v20.11.0现在支持 importModuleDynamically 选项。
v14.6.0现在支持 microtaskMode 选项。
v10.0.0第一个参数不再可以是函数。
v10.0.0现在支持 codeGeneration 选项。
v0.3.1新增于:v0.3.1
  • contextObject <Object> | <vm.constants.DONT_CONTEXTIFY> | <undefined> [vm.constants.DONT_CONTEXTIFY](/zh/api/vm#vmconstantsdont_contextify) 或一个将被 上下文化 的对象。如果为 undefined,则为向后兼容性创建一个空的上下文化对象。

  • options <Object>

    • name <string> 新创建上下文的易于理解的名称。默认值: 'VM Context i',其中 i 是已创建上下文的递增数字索引。

    • origin <string> 用于显示目的的对应于新创建上下文的 Origin。origin 应格式化为 URL,但仅包含方案、主机和端口(如有必要),例如 url.origin 属性(URL 对象)的值。最值得注意的是,此字符串应省略尾部斜杠,因为这表示路径。默认值: ''

    • codeGeneration <Object>

    • strings <boolean> 如果设置为 false,则对 eval 或函数构造函数(FunctionGeneratorFunction 等)的任何调用都将抛出 EvalError默认值: true

    • wasm <boolean> 如果设置为 false,则任何尝试编译 WebAssembly 模块都将抛出 WebAssembly.CompileError默认值: true

    • microtaskMode <string> 如果设置为 afterEvaluate,则微任务(通过 Promiseasync function 调度的任务)将在脚本通过 script.runInContext() 运行后立即运行。在这种情况下,它们包含在 timeoutbreakOnSigint 作用域中。

    • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在没有引用脚本或模块的情况下在上下文中调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。更多详细信息,请参阅 编译 API 中动态 import() 的支持

  • 返回: <Object> 上下文化对象。

如果给定的 contextObject 是一个对象,则 vm.createContext() 方法将 准备该对象 并返回对它的引用,以便它可以用于调用 vm.runInContext()script.runInContext()。在这些脚本内部,全局对象将被 contextObject 包装,保留其所有现有属性,但也具有任何标准 全局对象 拥有的内置对象和函数。在 vm 模块运行的脚本之外,全局变量将保持不变。

js
const vm = require('node:vm')

global.globalVar = 3

const context = { globalVar: 1 }
vm.createContext(context)

vm.runInContext('globalVar *= 2;', context)

console.log(context)
// 打印:{ globalVar: 2 }

console.log(global.globalVar)
// 打印:3

如果省略 contextObject(或显式地将其作为 undefined 传递),则将返回一个新的空 上下文化 对象。

当新创建上下文中的全局对象被 上下文化 时,它与普通的全局对象相比有一些怪癖。例如,它不能被冻结。要创建一个没有上下文化怪癖的上下文,请将 vm.constants.DONT_CONTEXTIFY 作为 contextObject 参数传递。有关详细信息,请参阅 vm.constants.DONT_CONTEXTIFY 的文档。

vm.createContext() 方法主要用于创建一个可以用于运行多个脚本的单个上下文。例如,如果模拟 Web 浏览器,则该方法可以用于创建一个表示窗口全局对象的单个上下文,然后在该上下文中一起运行所有 \<script\> 标签。

上下文提供的 nameorigin 通过 Inspector API 可见。

vm.isContext(object)

新增于:v0.11.7

如果给定的 object 对象已使用 vm.createContext() 进行过 上下文化,或者它是使用 vm.constants.DONT_CONTEXTIFY 创建的上下文的全局对象,则返回 true

vm.measureMemory([options])

新增于:v13.10.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1 - 实验性

测量 V8 已知并由当前 V8 隔离区中所有已知上下文或主上下文使用的内存。

  • options <Object> 可选。

    • mode <string> 'summary''detailed' 之一。在摘要模式下,只返回为主要上下文测量的内存。在详细模式下,将返回为当前 V8 隔离区中所有已知上下文测量的内存。默认值:'summary'
    • execution <string> 'default''eager' 之一。使用默认执行,promise 只有在下一次计划的垃圾回收开始后才会解析,这可能需要一段时间(如果程序在下一次 GC 之前退出,则可能永远不会)。使用紧急执行,将立即启动 GC 以测量内存。默认值:'default'
  • 返回值:<Promise> 如果内存成功测量,则 promise 将解析为包含有关内存使用情况的信息的对象。否则,它将被 ERR_CONTEXT_NOT_INITIALIZED 错误拒绝。

返回的 Promise 可能解析成的对象的格式特定于 V8 引擎,并且可能在 V8 的不同版本之间发生变化。

返回的结果与 v8.getHeapSpaceStatistics() 返回的统计信息不同,因为 vm.measureMemory() 测量当前 V8 引擎实例中每个 V8 特定上下文可访问的内存,而 v8.getHeapSpaceStatistics() 的结果测量当前 V8 实例中每个堆空间占用的内存。

js
const vm = require('node:vm')
// 测量主上下文使用的内存。
vm.measureMemory({ mode: 'summary' })
  // 这与 vm.measureMemory() 相同
  .then(result => {
    // 当前格式为:
    // {
    //   total: {
    //      jsMemoryEstimate: 2418479, jsMemoryRange: [ 2418479, 2745799 ]
    //    }
    // }
    console.log(result)
  })

const context = vm.createContext({ a: 1 })
vm.measureMemory({ mode: 'detailed', execution: 'eager' }).then(result => {
  // 在此处引用上下文,以便在测量完成之前不会对其进行 GC。
  console.log(context.a)
  // {
  //   total: {
  //     jsMemoryEstimate: 2574732,
  //     jsMemoryRange: [ 2574732, 2904372 ]
  //   },
  //   current: {
  //     jsMemoryEstimate: 2438996,
  //     jsMemoryRange: [ 2438996, 2768636 ]
  //   },
  //   other: [
  //     {
  //       jsMemoryEstimate: 135736,
  //       jsMemoryRange: [ 135736, 465376 ]
  //     }
  //   ]
  // }
  console.log(result)
})

vm.runInContext(code, contextifiedObject[, options])

[历史]

版本变更
v21.7.0, v20.12.0添加了对 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 的支持。
v17.0.0, v16.12.0添加了对 importModuleDynamically 参数的导入属性的支持。
v6.3.0现在支持 breakOnSigint 选项。
v0.3.1在 v0.3.1 中添加
  • code <字符串> 要编译和运行的 JavaScript 代码。
  • contextifiedObject <对象> 在编译和运行 code 时将用作 global上下文化 对象。
  • options <对象> | <字符串>
    • filename <字符串> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.\<anonymous\>'
    • lineOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
    • displayErrors <布尔值> 当值为 true 时,如果在编译 code 时发生 Error ,则导致错误的代码行将附加到堆栈跟踪中。默认值: true
    • timeout <整数> 指定在终止执行之前执行 code 的毫秒数。如果执行终止,则会抛出 Error 。此值必须为严格正整数。
    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出 Error 。在此脚本执行期间,通过 process.on('SIGINT') 附加的事件的现有处理程序将被禁用,但在之后继续工作。默认值: false
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArray,或带有 V8 代码缓存数据的 DataView
    • importModuleDynamically <函数> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在调用 import() 时如何加载此脚本评估期间的模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。有关详细信息,请参阅 编译 API 中动态 import() 的支持

vm.runInContext() 方法编译 code,在 contextifiedObject 的上下文中运行它,然后返回结果。运行的代码无法访问局部作用域。contextifiedObject 对象必须以前使用 vm.createContext() 方法进行 上下文化

如果 options 是一个字符串,则它指定文件名。

以下示例使用单个 上下文化 对象编译和执行不同的脚本:

js
const vm = require('node:vm')

const contextObject = { globalVar: 1 }
vm.createContext(contextObject)

for (let i = 0; i < 10; ++i) {
  vm.runInContext('globalVar *= 2;', contextObject)
}
console.log(contextObject)
// 打印:{ globalVar: 1024 }

vm.runInNewContext(code[, contextObject[, options]])

[历史]

版本变更
v22.8.0, v20.18.0contextObject 参数现在接受 vm.constants.DONT_CONTEXTIFY
v21.7.0, v20.12.0添加了对 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 的支持。
v17.0.0, v16.12.0importModuleDynamically 参数添加了对导入属性的支持。
v14.6.0现在支持 microtaskMode 选项。
v10.0.0现在支持 contextCodeGeneration 选项。
v6.3.0现在支持 breakOnSigint 选项。
v0.3.1在 v0.3.1 中添加
  • code <字符串> 要编译和运行的 JavaScript 代码。

  • contextObject <对象> | <vm.constants.DONT_CONTEXTIFY> | <未定义> vm.constants.DONT_CONTEXTIFY 或将被上下文化 的对象。如果为 undefined,则为了向后兼容性,将创建一个空的上下文化对象。

  • options <对象> | <字符串>

    • filename <字符串> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.<anonymous>'

    • lineOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0

    • columnOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0

    • displayErrors <布尔值> 如果为 true,则当编译 code 时发生 Error 时,导致错误的代码行将附加到堆栈跟踪中。默认值: true

    • timeout <整数> 指定在终止执行之前执行 code 的毫秒数。如果执行被终止,则会抛出 Error。此值必须为严格的正整数。

    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出 Error。在此脚本执行期间,通过 process.on('SIGINT') 附加的事件的现有处理程序将被禁用,但在之后继续工作。默认值: false

    • contextName <字符串> 新创建上下文的易于理解的名称。默认值: 'VM Context i',其中 i 是已创建上下文的递增数字索引。

    • contextOrigin <字符串> 用于显示目的的新创建上下文的来源。来源应像 URL 一样格式化,但仅包含方案、主机和端口(如有必要),例如 url.origin 属性(URL 对象)的值。最值得注意的是,此字符串应省略尾部斜杠,因为它表示路径。默认值: ''

    • contextCodeGeneration <对象>

    • strings <布尔值> 如果设置为 false,则对 eval 或函数构造函数(FunctionGeneratorFunction 等)的任何调用都将抛出 EvalError默认值: true

    • wasm <布尔值> 如果设置为 false,则任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true

    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArrayDataView,其中包含 V8 的代码缓存数据,用于提供的源代码。

    • importModuleDynamically <函数> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定调用 import() 时应如何加载此脚本的评估期间的模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。有关详细信息,请参阅编译 API 中动态 import() 的支持

    • microtaskMode <字符串> 如果设置为 afterEvaluate,则微任务(通过 Promiseasync function 调度的任务)将在脚本运行后立即运行。在这种情况下,它们包含在 timeoutbreakOnSigint 范围内。

  • 返回: <任意> 脚本中执行的最后一个语句的结果。

此方法是 (new vm.Script(code, options)).runInContext(vm.createContext(options), options) 的快捷方式。如果 options 是字符串,则它指定文件名。

它一次执行几件事:

以下示例编译并执行递增全局变量并设置新全局变量的代码。这些全局变量包含在 contextObject 中。

js
const vm = require('node:vm')

const contextObject = {
  animal: 'cat',
  count: 2,
}

vm.runInNewContext('count += 1; name = "kitty"', contextObject)
console.log(contextObject)
// 打印:{ animal: 'cat', count: 3, name: 'kitty' }

// 如果上下文是从上下文化对象创建的,则会抛出此异常。
// vm.constants.DONT_CONTEXTIFY 允许使用可以冻结的普通全局对象创建上下文。
const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY)

vm.runInThisContext(code[, options])

[历史]

版本变更
v21.7.0, v20.12.0添加了对 vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER 的支持。
v17.0.0, v16.12.0importModuleDynamically 参数添加了对导入属性的支持。
v6.3.0现在支持 breakOnSigint 选项。
v0.3.1在 v0.3.1 中添加
  • code <字符串> 要编译和运行的 JavaScript 代码。

  • options <对象> | <字符串>

    • filename <字符串> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.<anonymous>'
    • lineOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <数字> 指定在此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
    • displayErrors <布尔值> 当值为 true 时,如果在编译 code 时发生 Error 错误,则导致错误的代码行将附加到堆栈跟踪中。默认值: true
    • timeout <整数> 指定在终止执行之前执行 code 的毫秒数。如果执行终止,则会抛出 Error 错误。此值必须是严格的正整数。
    • breakOnSigint <布尔值> 如果为 true,则接收 SIGINT (+) 将终止执行并抛出 Error 错误。在此脚本执行期间,通过 process.on('SIGINT') 附加的事件的现有处理程序将被禁用,但在之后继续工作。默认值: false
    • cachedData <Buffer> | <TypedArray> | <DataView> 提供可选的 BufferTypedArray,或带有 V8 代码缓存数据的 DataView
    • importModuleDynamically <函数> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在调用 import() 时应如何加载模块,以便评估此脚本。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。有关详细信息,请参见编译 API 中动态 import() 的支持
  • 返回值:<任意值> 脚本中执行的最后一个语句的结果。

vm.runInThisContext() 编译 code,在当前 global 的上下文中运行它并返回结果。运行的代码无法访问局部作用域,但可以访问当前的 global 对象。

如果 options 是一个字符串,则它指定文件名。

以下示例演示了使用 vm.runInThisContext() 和 JavaScript eval() 函数运行相同代码:

js
const vm = require('node:vm')
let localVar = 'initial value'

const vmResult = vm.runInThisContext('localVar = "vm";')
console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`)
// 输出:vmResult: 'vm', localVar: 'initial value'

const evalResult = eval('localVar = "eval";')
console.log(`evalResult: '${evalResult}', localVar: '${localVar}'`)
// 输出:evalResult: 'eval', localVar: 'eval'

因为 vm.runInThisContext() 无法访问局部作用域,所以 localVar 保持不变。相反,eval() 可以访问局部作用域,因此 localVar 的值会更改。这样,vm.runInThisContext() 就非常类似于间接 eval() 调用,例如 (0,eval)('code')

示例:在虚拟机中运行 HTTP 服务器

使用 script.runInThisContext()vm.runInThisContext() 时,代码在当前 V8 全局上下文中执行。传递给此 VM 上下文的代码将拥有其自身隔离的范围。

为了使用 node:http 模块运行简单的 Web 服务器,传递给上下文中的代码必须自行调用 require('node:http'),或者具有传递给它的 node:http 模块的引用。例如:

js
'use strict'
const vm = require('node:vm')

const code = `
((require) => {
  const http = require('node:http');

  http.createServer((request, response) => {
    response.writeHead(200, { 'Content-Type': 'text/plain' });
    response.end('Hello World\\n');
  }).listen(8124);

  console.log('Server running at http://127.0.0.1:8124/');
})`

vm.runInThisContext(code)(require)

上述情况中的 require() 与传递它的上下文共享状态。当执行不受信任的代码时,这可能会带来风险,例如以不需要的方式更改上下文中的对象。

“上下文化” 对象意味着什么?

所有在 Node.js 中执行的 JavaScript 代码都在一个“上下文”的范围内运行。根据 V8 嵌入器指南

当使用对象调用 vm.createContext() 方法时,contextObject 参数将用于包装 V8 上下文的新的实例的全局对象(如果 contextObjectundefined,则会在其上下文化之前从当前上下文创建一个新对象)。此 V8 上下文为使用 node:vm 模块的方法运行的 code 提供了一个隔离的全局环境,它可以在其中运行。创建 V8 上下文并将其与外部上下文中的 contextObject 关联的过程,本文档称之为对象的“上下文化”。

上下文化会在上下文的 globalThis 值中引入一些特性。例如,它不能被冻结,并且它与外部上下文中的 contextObject 不引用相等。

js
const vm = require('node:vm')

// 未定义的 `contextObject` 选项使全局对象上下文化。
const context = vm.createContext()
console.log(vm.runInContext('globalThis', context) === context) // false
// 上下文化的全局对象不能被冻结。
try {
  vm.runInContext('Object.freeze(globalThis);', context)
} catch (e) {
  console.log(e) // TypeError: Cannot freeze
}
console.log(vm.runInContext('globalThis.foo = 1; foo;', context)) // 1

要创建一个具有普通全局对象的上下文,并获得对外部上下文中的全局代理的访问权限,且具有较少的特性,请将 vm.constants.DONT_CONTEXTIFY 指定为 contextObject 参数。

vm.constants.DONT_CONTEXTIFY

此常量在 vm API 中用作 contextObject 参数时,会指示 Node.js 创建一个上下文,而不以 Node.js 特有的方式将其全局对象包装在另一个对象中。因此,新上下文中的 globalThis 值的行为更接近于普通全局对象。

js
const vm = require('node:vm')

// 使用 vm.constants.DONT_CONTEXTIFY 冻结全局对象。
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY)
vm.runInContext('Object.freeze(globalThis);', context)
try {
  vm.runInContext('bar = 1; bar;', context)
} catch (e) {
  console.log(e) // Uncaught ReferenceError: bar is not defined
}

vm.constants.DONT_CONTEXTIFY 用作 vm.createContext()contextObject 参数时,返回的对象是新创建上下文中的全局对象的类似代理的对象,具有较少的 Node.js 特有的特性。它与新上下文中的 globalThis 值引用相等,可以从上下文外部修改,并且可以用于直接访问新上下文中的内置对象。

js
const vm = require('node:vm')

const context = vm.createContext(vm.constants.DONT_CONTEXTIFY)

// 返回的对象与新上下文中的 globalThis 引用相等。
console.log(vm.runInContext('globalThis', context) === context) // true

// 可用于直接访问新上下文中的全局变量。
console.log(context.Array) // [Function: Array]
vm.runInContext('foo = 1;', context)
console.log(context.foo) // 1
context.bar = 1
console.log(vm.runInContext('bar;', context)) // 1

// 可以被冻结,并且会影响内部上下文。
Object.freeze(context)
try {
  vm.runInContext('baz = 1; baz;', context)
} catch (e) {
  console.log(e) // Uncaught ReferenceError: baz is not defined
}

异步任务和 Promise 的超时交互

Promiseasync function 可以调度 JavaScript 引擎异步运行的任务。默认情况下,这些任务会在当前堆栈上的所有 JavaScript 函数执行完毕后再运行。这允许绕过 timeoutbreakOnSigint 选项的功能。

例如,以下由 vm.runInNewContext() 执行的代码,超时时间为 5 毫秒,它会安排一个无限循环在 promise 解析后运行。这个计划好的循环不会被超时中断:

js
const vm = require('node:vm')

function loop() {
  console.log('entering loop')
  while (1) console.log(Date.now())
}

vm.runInNewContext('Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5 })
// 这会在 'entering loop' 之前打印 (!)
console.log('done executing')

这可以通过将 microtaskMode: 'afterEvaluate' 传递给创建 Context 的代码来解决:

js
const vm = require('node:vm')

function loop() {
  while (1) console.log(Date.now())
}

vm.runInNewContext(
  'Promise.resolve().then(() => loop());',
  { loop, console },
  { timeout: 5, microtaskMode: 'afterEvaluate' }
)

在这种情况下,通过 promise.then() 调度的微任务将在从 vm.runInNewContext() 返回之前运行,并将被 timeout 功能中断。这仅适用于在 vm.Context 中运行的代码,例如 vm.runInThisContext() 不会采用此选项。

Promise 回调被放入创建它们的上下文中的微任务队列中。例如,如果在上面的例子中将 () => loop() 替换为 loop,那么 loop 将被推入全局微任务队列,因为它来自外部(主)上下文,因此也能逃过超时。

如果在 vm.Context 中提供了异步调度函数,例如 process.nextTick()queueMicrotask()setTimeout()setImmediate() 等,则传递给它们的函数将被添加到全局队列中,这些队列由所有上下文共享。因此,传递给这些函数的回调也不能通过超时来控制。

编译 API 中对动态 import() 的支持

以下 API 支持 importModuleDynamically 选项,以启用由 vm 模块编译的代码中的动态 import()

  • new vm.Script
  • vm.compileFunction()
  • new vm.SourceTextModule
  • vm.runInThisContext()
  • vm.runInContext()
  • vm.runInNewContext()
  • vm.createContext()

此选项仍然是实验性模块 API 的一部分。我们不建议在生产环境中使用它。

未指定或未定义 importModuleDynamically 选项时

如果未指定此选项,或如果它是 undefined,则包含 import() 的代码仍然可以由 vm API 编译,但是当编译后的代码被执行并且它实际调用 import() 时,结果将被拒绝,并显示 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING

importModuleDynamicallyvm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER

此选项当前不支持 vm.SourceTextModule

使用此选项时,当在编译后的代码中启动 import() 时,Node.js 将使用主上下文中的默认 ESM 加载器来加载请求的模块并将其返回给正在执行的代码。

这允许访问 Node.js 内置模块,例如 fshttp,以供编译的代码使用。如果代码在不同的上下文中执行,请注意,从主上下文加载的模块创建的对象仍然来自主上下文,而不是新上下文中的内置类的 instanceof

js
const { Script, constants } = require('node:vm')
const script = new Script('import("node:fs").then(({readFile}) => readFile instanceof Function)', {
  importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
})

// false: 从主上下文加载的 URL 不是新上下文中的 Function
// 类的实例。
script.runInNewContext().then(console.log)
js
import { Script, constants } from 'node:vm'

const script = new Script('import("node:fs").then(({readFile}) => readFile instanceof Function)', {
  importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
})

// false: 从主上下文加载的 URL 不是新上下文中的 Function
// 类的实例。
script.runInNewContext().then(console.log)

此选项还允许脚本或函数加载用户模块:

js
import { Script, constants } from 'node:vm'
import { resolve } from 'node:path'
import { writeFileSync } from 'node:fs'

// 将 test.js 和 test.txt 写入当前运行脚本所在的目录。
writeFileSync(resolve(import.meta.dirname, 'test.mjs'), 'export const filename = "./test.json";')
writeFileSync(resolve(import.meta.dirname, 'test.json'), '{"hello": "world"}')

// 编译一个脚本,该脚本加载 test.mjs,然后加载 test.json,
// 就好像脚本位于同一个目录中一样。
const script = new Script(
  `(async function() {
    const { filename } = await import('./test.mjs');
    return import(filename, { with: { type: 'json' } })
  })();`,
  {
    filename: resolve(import.meta.dirname, 'test-with-default.js'),
    importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
  }
)

// { default: { hello: 'world' } }
script.runInThisContext().then(console.log)
js
const { Script, constants } = require('node:vm')
const { resolve } = require('node:path')
const { writeFileSync } = require('node:fs')

// 将 test.js 和 test.txt 写入当前运行脚本所在的目录。
writeFileSync(resolve(__dirname, 'test.mjs'), 'export const filename = "./test.json";')
writeFileSync(resolve(__dirname, 'test.json'), '{"hello": "world"}')

// 编译一个脚本,该脚本加载 test.mjs,然后加载 test.json,
// 就好像脚本位于同一个目录中一样。
const script = new Script(
  `(async function() {
    const { filename } = await import('./test.mjs');
    return import(filename, { with: { type: 'json' } })
  })();`,
  {
    filename: resolve(__dirname, 'test-with-default.js'),
    importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
  }
)

// { default: { hello: 'world' } }
script.runInThisContext().then(console.log)

使用主上下文中的默认加载器加载用户模块有一些注意事项:

importModuleDynamically 是一个函数时

importModuleDynamically 是一个函数时,它会在编译后的代码中调用 import() 时被调用,以便用户自定义请求的模块应该如何编译和评估。目前,Node.js 实例必须使用 --experimental-vm-modules 标志启动才能使此选项生效。如果未设置此标志,则此回调将被忽略。如果实际评估的代码调用了 import(),则结果将拒绝,并返回 ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG

回调 importModuleDynamically(specifier, referrer, importAttributes) 具有以下签名:

  • specifier <string> 传递给 import() 的说明符
  • referrer <vm.Script> | <Function> | <vm.SourceTextModule> | <Object> referrernew vm.Scriptvm.runInThisContextvm.runInContextvm.runInNewContext 的已编译 vm.Script。对于 vm.compileFunction,它是已编译的 Function;对于 new vm.SourceTextModule,它是已编译的 vm.SourceTextModule;对于 vm.createContext(),它是上下文 Object
  • importAttributes <Object> 传递给 optionsExpression 可选参数的 "with" 值,如果没有提供值,则为空对象。
  • 返回值: <Module Namespace Object> | <vm.Module> 建议返回 vm.Module 以利用错误跟踪,并避免出现包含 then 函数导出的命名空间问题。
js
// 此脚本必须使用 --experimental-vm-modules 运行。
import { Script, SyntheticModule } from 'node:vm'

const script = new Script('import("foo.json", { with: { type: "json" } })', {
  async importModuleDynamically(specifier, referrer, importAttributes) {
    console.log(specifier) // 'foo.json'
    console.log(referrer) // 已编译的脚本
    console.log(importAttributes) // { type: 'json' }
    const m = new SyntheticModule(['bar'], () => {})
    await m.link(() => {})
    m.setExport('bar', { hello: 'world' })
    return m
  },
})
const result = await script.runInThisContext()
console.log(result) //  { bar: { hello: 'world' } }
js
// 此脚本必须使用 --experimental-vm-modules 运行。
const { Script, SyntheticModule } = require('node:vm')

;(async function main() {
  const script = new Script('import("foo.json", { with: { type: "json" } })', {
    async importModuleDynamically(specifier, referrer, importAttributes) {
      console.log(specifier) // 'foo.json'
      console.log(referrer) // 已编译的脚本
      console.log(importAttributes) // { type: 'json' }
      const m = new SyntheticModule(['bar'], () => {})
      await m.link(() => {})
      m.setExport('bar', { hello: 'world' })
      return m
    },
  })
  const result = await script.runInThisContext()
  console.log(result) //  { bar: { hello: 'world' } }
})()