VM (JavaScript 执行)#

稳定性:2 - 稳定

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

node:vm 模块不是安全机制。请勿使用它来运行不可信代码。

JavaScript 代码可以立即编译并运行,也可以编译后保存,留待稍后运行。

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

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

import { createContext, runInContext } from 'node:vm';

const x = 1;

const context = { x: 2 };
createContext(context); // Contextify the object.

const code = 'x += 40; var y = 17;';
// `x` and `y` are global variables in the context.
// Initially, x has the value 2 because that is the value of context.x.
runInContext(code, context);

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

console.log(x); // 1; y is not defined
const { createContext, runInContext } = require('node:vm');

const x = 1;

const context = { x: 2 };
createContext(context); // Contextify the object.

const code = 'x += 40; var y = 17;';
// `x` and `y` are global variables in the context.
// Initially, x has the value 2 because that is the value of context.x.
runInContext(code, context);

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

console.log(x); // 1; y is not defined

类:vm.Script#

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

new vm.Script(code[, options])#

  • code <string> 要编译的 JavaScript 代码。
  • options <Object> | <string>
    • filename <string> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.<anonymous>'
    • lineOffset <number> 指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <number> 指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
    • cachedData <Buffer> | <TypedArray> | <DataView> 为提供的源代码提供可选的 BufferTypedArrayDataView,其中包含 V8 的代码缓存数据。提供此数据后,cachedDataRejected 的值将根据 V8 是否接受该数据被设置为 truefalse
    • produceCachedData <boolean> 当为 true 且不存在 cachedData 时,V8 将尝试为 code 生成代码缓存数据。成功后,包含 V8 代码缓存数据的 Buffer 将被生成并存储在返回的 vm.Script 实例的 cachedData 属性中。cachedDataProduced 的值将根据代码缓存数据是否成功生成被设置为 truefalse。此选项已弃用,建议改用 script.createCachedData()默认值: false
    • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在评估此脚本期间调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态 import() 的支持

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

创建新的 vm.Script 对象会编译 code 但不会运行它。编译后的 vm.Script 可以在稍后多次运行。code 不绑定到任何全局对象;相反,它在每次运行前都会绑定,仅针对那次运行。

script.cachedDataRejected#

当创建 vm.Script 时提供了 cachedData,此值将根据 V8 是否接受该数据被设置为 truefalse。否则该值为 undefined

script.createCachedData()#

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

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

Script 源码中的函数可以被标记为延迟编译,它们在 Script 构建时不会被编译。这些函数将在首次调用时被编译。代码缓存序列化了 V8 当前已知的关于该 Script 的元数据,可用于加速未来的编译。

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

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

const cacheWithoutAdd = script.createCachedData();
// In `cacheWithoutAdd` the function `add()` is marked for full compilation
// upon invocation.

script.runInThisContext();

const cacheWithAdd = script.createCachedData();
// `cacheWithAdd` contains fully compiled function `add()`.

script.runInContext(contextifiedObject[, options])#

  • contextifiedObject <Object>vm.createContext() 方法返回的已上下文化的对象。
  • options <Object>
  • displayErrors <boolean> 当为 true 时,如果编译 code 时发生 Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值: true
  • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出 Error。该值必须是严格的正整数。
  • breakOnSigint <boolean> 如果为 true,接收 SIGINT (Ctrl+C) 将终止执行并抛出 Error。通过 process.on('SIGINT') 附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值: false
  • 返回:<any> 脚本中执行的最后一条语句的结果。
  • 在给定的 contextifiedObject 中运行 vm.Script 对象中包含的已编译代码,并返回结果。运行的代码无法访问本地作用域。

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

    import { createContext, Script } from 'node:vm';
    
    const context = {
      animal: 'cat',
      count: 2,
    };
    
    const script = new Script('count += 1; name = "kitty";');
    
    createContext(context);
    for (let i = 0; i < 10; ++i) {
      script.runInContext(context);
    }
    
    console.log(context);
    // Prints: { animal: 'cat', count: 12, name: 'kitty' }
    const { createContext, Script } = require('node:vm');
    
    const context = {
      animal: 'cat',
      count: 2,
    };
    
    const script = new Script('count += 1; name = "kitty";');
    
    createContext(context);
    for (let i = 0; i < 10; ++i) {
      script.runInContext(context);
    }
    
    console.log(context);
    // Prints: { animal: 'cat', count: 12, name: 'kitty' }
    

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

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

    • contextObject <Object> | <vm.constants.DONT_CONTEXTIFY> | <undefined> 要么是 vm.constants.DONT_CONTEXTIFY,要么是一个将被上下文化的对象。如果为 undefined,将创建一个空的已上下文化对象以实现向后兼容。
    • options <Object>
    • displayErrors <boolean> 当为 true 时,如果编译 code 时发生 Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值: true
    • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出 Error。该值必须是严格的正整数。
    • breakOnSigint <boolean> 如果为 true,接收 SIGINT (Ctrl+C) 将终止执行并抛出 Error。通过 process.on('SIGINT') 附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值: false
    • contextName <string> 新创建上下文的可读名称。默认值: 'VM Context i',其中 i 是创建上下文的递增数字索引。
    • contextOrigin <string> 用于显示目的、与新创建上下文相对应的源(Origin)。源应格式化为 URL,但仅包含方案、主机和端口(如有必要),如 URL 对象的 url.origin 属性值。最重要的是,此字符串应省略尾随斜杠,因为它表示路径。默认值: ''
    • contextCodeGeneration <Object>
      • strings <boolean> 如果设为 false,任何对 eval 或函数构造函数(FunctionGeneratorFunction 等)的调用都将抛出 EvalError默认值: true
      • wasm <boolean> 如果设为 false,任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true
    • microtaskMode <string> 如果设为 afterEvaluate,微任务(通过 Promiseasync function 调度的任务)将在脚本运行后立即运行。在这种情况下,它们会被包含在 timeoutbreakOnSigint 的作用域中。
  • 返回:<any> 脚本中执行的最后一条语句的结果。
  • 此方法是 script.runInContext(vm.createContext(options), options) 的快捷方式。它同时执行多项操作:

    1. 创建一个新上下文。
    2. 如果 contextObject 是对象,则使用新上下文将其上下文化。如果 contextObject 未定义,则创建一个新对象并将其上下文化。如果 contextObjectvm.constants.DONT_CONTEXTIFY,则不上下文化任何东西。
    3. 在创建的上下文中运行 vm.Script 对象中包含的已编译代码。该代码无法访问调用此方法的作用域。
    4. 返回结果。

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

    import { constants, Script } from 'node:vm';
    
    const script = new Script('globalVar = "set"');
    
    const contexts = [{}, {}, {}];
    contexts.forEach((context) => {
      script.runInNewContext(context);
    });
    
    console.log(contexts);
    // Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]
    
    // This would throw if the context is created from a contextified object.
    // constants.DONT_CONTEXTIFY allows creating contexts with ordinary
    // global objects that can be frozen.
    const freezeScript = new Script('Object.freeze(globalThis); globalThis;');
    const frozenContext = freezeScript.runInNewContext(constants.DONT_CONTEXTIFY);
    const { constants, Script } = require('node:vm');
    
    const script = new Script('globalVar = "set"');
    
    const contexts = [{}, {}, {}];
    contexts.forEach((context) => {
      script.runInNewContext(context);
    });
    
    console.log(contexts);
    // Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]
    
    // This would throw if the context is created from a contextified object.
    // constants.DONT_CONTEXTIFY allows creating contexts with ordinary
    // global objects that can be frozen.
    const freezeScript = new Script('Object.freeze(globalThis); globalThis;');
    const frozenContext = freezeScript.runInNewContext(constants.DONT_CONTEXTIFY);
    

    script.runInThisContext([options])#

    • options <Object>
    • displayErrors <boolean> 当为 true 时,如果编译 code 时发生 Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值: true
    • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出 Error。该值必须是严格的正整数。
    • breakOnSigint <boolean> 如果为 true,接收 SIGINT (Ctrl+C) 将终止执行并抛出 Error。通过 process.on('SIGINT') 附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值: false
  • 返回:<any> 脚本中执行的最后一条语句的结果。
  • 在当前 global 对象的上下文中运行 vm.Script 对象中包含的已编译代码。运行的代码无法访问本地作用域,但可以访问当前的 global 对象。

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

    import { Script } from 'node:vm';
    
    global.globalVar = 0;
    
    const script = new Script('globalVar += 1', { filename: 'myfile.vm' });
    
    for (let i = 0; i < 1000; ++i) {
      script.runInThisContext();
    }
    
    console.log(globalVar);
    
    // 1000
    const { Script } = require('node:vm');
    
    global.globalVar = 0;
    
    const script = new Script('globalVar += 1', { filename: 'myfile.vm' });
    
    for (let i = 0; i < 1000; ++i) {
      script.runInThisContext();
    }
    
    console.log(globalVar);
    
    // 1000
    

    script.sourceMapURL#

    当脚本从包含源映射(source map)魔术注释的源编译而来时,此属性将被设置为源映射的 URL。

    import vm from 'node:vm';
    
    const script = new vm.Script(`
    function myFunc() {}
    //# sourceMappingURL=sourcemap.json
    `);
    
    console.log(script.sourceMapURL);
    // Prints: sourcemap.json
    const vm = require('node:vm');
    
    const script = new vm.Script(`
    function myFunc() {}
    //# sourceMappingURL=sourcemap.json
    `);
    
    console.log(script.sourceMapURL);
    // Prints: sourcemap.json
    

    类:vm.Module#

    稳定性:1 - 实验性

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

    vm.Module 类提供了一个用于在 VM 上下文中使用 ECMAScript 模块的底层接口。它是 vm.Script 类的对应物,紧密映射了 ECMAScript 规范中定义的模块记录(Module Record)

    然而,与 vm.Script 不同,每个 vm.Module 对象从创建起就绑定到一个上下文。

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

    此实现处于比 ECMAScript 模块加载器 更底层的级别。目前还没有办法与加载器交互,但计划提供支持。

    import vm from 'node:vm';
    
    const contextifiedObject = vm.createContext({
      secret: 42,
      print: console.log,
    });
    
    // Step 1
    //
    // Create a Module by constructing a new `vm.SourceTextModule` object. This
    // parses the provided source text, throwing a `SyntaxError` if anything goes
    // wrong. By default, a Module is created in the top context. But here, we
    // specify `contextifiedObject` as the context this Module belongs to.
    //
    // Here, we attempt to obtain the default export from the module "foo", and
    // put it into local binding "secret".
    
    const rootModule = new vm.SourceTextModule(`
      import s from 'foo';
      s;
      print(s);
    `, { context: contextifiedObject });
    
    // Step 2
    //
    // "Link" the imported dependencies of this Module to it.
    //
    // Obtain the requested dependencies of a SourceTextModule by
    // `sourceTextModule.moduleRequests` and resolve them.
    //
    // Even top-level Modules without dependencies must be explicitly linked. The
    // array passed to `sourceTextModule.linkRequests(modules)` can be
    // empty, however.
    //
    // Note: This is a contrived example in that the resolveAndLinkDependencies
    // creates a new "foo" module every time it is called. In a full-fledged
    // module system, a cache would probably be used to avoid duplicated modules.
    
    const moduleMap = new Map([
      ['root', rootModule],
    ]);
    
    function resolveAndLinkDependencies(module) {
      const requestedModules = module.moduleRequests.map((request) => {
        // In a full-fledged module system, the resolveAndLinkDependencies would
        // resolve the module with the module cache key `[specifier, attributes]`.
        // In this example, we just use the specifier as the key.
        const specifier = request.specifier;
    
        let requestedModule = moduleMap.get(specifier);
        if (requestedModule === undefined) {
          requestedModule = new vm.SourceTextModule(`
            // The "secret" variable refers to the global variable we added to
            // "contextifiedObject" when creating the context.
            export default secret;
          `, { context: module.context });
          moduleMap.set(specifier, requestedModule);
          // Resolve the dependencies of the new module as well.
          resolveAndLinkDependencies(requestedModule);
        }
    
        return requestedModule;
      });
    
      module.linkRequests(requestedModules);
    }
    
    resolveAndLinkDependencies(rootModule);
    rootModule.instantiate();
    
    // Step 3
    //
    // Evaluate the Module. The evaluate() method returns a promise which will
    // resolve after the module has finished evaluating.
    
    // Prints 42.
    await rootModule.evaluate();
    const vm = require('node:vm');
    
    const contextifiedObject = vm.createContext({
      secret: 42,
      print: console.log,
    });
    
    (async () => {
      // Step 1
      //
      // Create a Module by constructing a new `vm.SourceTextModule` object. This
      // parses the provided source text, throwing a `SyntaxError` if anything goes
      // wrong. By default, a Module is created in the top context. But here, we
      // specify `contextifiedObject` as the context this Module belongs to.
      //
      // Here, we attempt to obtain the default export from the module "foo", and
      // put it into local binding "secret".
    
      const rootModule = new vm.SourceTextModule(`
        import s from 'foo';
        s;
        print(s);
      `, { context: contextifiedObject });
    
      // Step 2
      //
      // "Link" the imported dependencies of this Module to it.
      //
      // Obtain the requested dependencies of a SourceTextModule by
      // `sourceTextModule.moduleRequests` and resolve them.
      //
      // Even top-level Modules without dependencies must be explicitly linked. The
      // array passed to `sourceTextModule.linkRequests(modules)` can be
      // empty, however.
      //
      // Note: This is a contrived example in that the resolveAndLinkDependencies
      // creates a new "foo" module every time it is called. In a full-fledged
      // module system, a cache would probably be used to avoid duplicated modules.
    
      const moduleMap = new Map([
        ['root', rootModule],
      ]);
    
      function resolveAndLinkDependencies(module) {
        const requestedModules = module.moduleRequests.map((request) => {
          // In a full-fledged module system, the resolveAndLinkDependencies would
          // resolve the module with the module cache key `[specifier, attributes]`.
          // In this example, we just use the specifier as the key.
          const specifier = request.specifier;
    
          let requestedModule = moduleMap.get(specifier);
          if (requestedModule === undefined) {
            requestedModule = new vm.SourceTextModule(`
              // The "secret" variable refers to the global variable we added to
              // "contextifiedObject" when creating the context.
              export default secret;
            `, { context: module.context });
            moduleMap.set(specifier, requestedModule);
            // Resolve the dependencies of the new module as well.
            resolveAndLinkDependencies(requestedModule);
          }
    
          return requestedModule;
        });
    
        module.linkRequests(requestedModules);
      }
    
      resolveAndLinkDependencies(rootModule);
      rootModule.instantiate();
    
      // Step 3
      //
      // Evaluate the Module. The evaluate() method returns a promise which will
      // resolve after the module has finished evaluating.
    
      // Prints 42.
      await rootModule.evaluate();
    })();
    

    module.error#

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

    在没有抛出异常的情况下,不能使用 undefined 值,因为这可能与 throw undefined; 产生歧义。

    对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)[[EvaluationError]] 字段。

    module.evaluate([options])#

    • options <Object>
    • timeout <integer> 指定在终止执行前评估的毫秒数。如果执行被中断,将抛出 Error。该值必须是严格的正整数。
    • breakOnSigint <boolean> 如果为 true,接收 SIGINT (Ctrl+C) 将终止执行并抛出 Error。通过 process.on('SIGINT') 附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值: false
  • 返回:<Promise> 成功时以 undefined 兑现。
  • 评估模块及其依赖项。对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)Evaluate() 具体方法字段。

    如果模块是 vm.SourceTextModule,则必须在实例化模块后调用 evaluate();否则 evaluate() 将返回一个拒绝的 Promise。

    对于 vm.SourceTextModuleevaluate() 返回的 Promise 可以同步或异步完成:

    1. 如果 vm.SourceTextModule 本身或其任何依赖项中没有顶级 await,则 Promise 将在模块及其所有依赖项评估完成后同步完成。
    2. 如果评估成功,Promise 将同步解析为 undefined
    3. 如果评估导致异常,Promise 将同步拒绝并抛出导致评估失败的异常,这与 module.error 相同。
  • 如果 vm.SourceTextModule 本身或其任何依赖项中有顶级 await,则 Promise 将在模块及其所有依赖项评估完成后异步完成。
  • 如果评估成功,Promise 将异步解析为 undefined
  • 如果评估导致异常,Promise 将异步拒绝并抛出导致评估失败的异常。
  • 如果模块是 vm.SyntheticModuleevaluate() 总是返回一个同步完成的 Promise,请参阅 合成模块记录的 Evaluate() 规范。

    1. 如果传递给其构造函数的 evaluateCallback 同步抛出异常,evaluate() 将返回一个被该异常同步拒绝的 Promise。
    2. 如果 evaluateCallback 未抛出异常,evaluate() 将返回一个被同步解析为 undefined 的 Promise。

    vm.SyntheticModuleevaluateCallbackevaluate() 调用中同步执行,其返回值会被丢弃。这意味着如果 evaluateCallback 是一个异步函数,evaluate() 返回的 Promise 将不会反映其异步行为,且来自异步 evaluateCallback 的任何拒绝都将丢失。

    evaluate() 也可以在模块已经被评估后再次调用,在这种情况下:

    1. 如果初始评估成功(module.status'evaluated'),它将不执行任何操作并返回一个解析为 undefined 的 Promise。
    2. 如果初始评估导致异常(module.status'errored'),它将重新拒绝初始评估导致的异常。

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

    module.identifier#

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

    module.link(linker)#

    • linker <Function>
      • specifier <string> 所请求模块的说明符

        import foo from 'foo';
        //              ^^^^^ the module specifier
        
      • referencingModule <vm.Module> 调用 link()Module 对象。

      • extra <Object>

        • attributes <Object> 来自属性的数据import foo from 'foo' with { name: 'value' }; // ^^^^^^^^^^^^^^^^^ the attribute 根据 ECMA-262,如果存在不受支持的属性,宿主应触发错误。
        • assert <Object> extra.attributes 的别名。
      • 返回:<vm.Module> | <Promise>

    • 返回:<Promise>

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

    使用 sourceTextModule.linkRequests(modules)sourceTextModule.instantiate() 同步或异步链接模块。

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

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

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

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

    linker 函数大致对应于 ECMAScript 规范中实现定义的 HostResolveImportedModule 抽象操作,但有几个关键区别:

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

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

    module.namespace#

    模块的命名空间对象。这仅在链接(module.link())完成后可用。

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

    module.status#

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

    • 'unlinked':尚未调用 module.link()

    • 'linking':已调用 module.link(),但 linker 函数返回的所有 Promise 尚未全部解析。

    • 'linked':模块已成功链接,其所有依赖项也都已链接,但尚未调用 module.evaluate()

    • 'evaluating':模块正在通过其自身或父模块上的 module.evaluate() 进行评估。

    • 'evaluated':模块已成功评估。

    • 'errored':模块已评估,但抛出了异常。

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

    类:vm.SourceTextModule#

    稳定性:1 - 实验性

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

    vm.SourceTextModule 类提供 ECMAScript 规范中定义的源代码文本模块记录(Source Text Module Record)

    new vm.SourceTextModule(code[, options])#

    • 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() 在特定上下文中创建对象。

    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) {
          // Note: this object is created in the top context. As such,
          // Object.getPrototypeOf(import.meta.prop) points to the
          // Object.prototype in the top context rather than that in
          // the contextified object.
          meta.prop = {};
        },
      });
    // The module has an empty `moduleRequests` array.
    module.linkRequests([]);
    module.instantiate();
    await module.evaluate();
    
    // Now, Object.prototype.secret will be equal to 42.
    //
    // To fix this problem, replace
    //     meta.prop = {};
    // above with
    //     meta.prop = vm.runInContext('{}', contextifiedObject);
    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) {
            // Note: this object is created in the top context. As such,
            // Object.getPrototypeOf(import.meta.prop) points to the
            // Object.prototype in the top context rather than that in
            // the contextified object.
            meta.prop = {};
          },
        });
      // The module has an empty `moduleRequests` array.
      module.linkRequests([]);
      module.instantiate();
      await module.evaluate();
      // Now, Object.prototype.secret will be equal to 42.
      //
      // To fix this problem, replace
      //     meta.prop = {};
      // above with
      //     meta.prop = vm.runInContext('{}', contextifiedObject);
    })();
    

    sourceTextModule.createCachedData()#

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

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

    SourceTextModule 源码中的函数可以被标记为延迟编译,它们在 SourceTextModule 构建时不会被编译。这些函数将在首次调用时被编译。代码缓存序列化了 V8 当前已知的关于该 SourceTextModule 的元数据,可用于加速未来的编译。

    // Create an initial module
    const module = new vm.SourceTextModule('const a = 1;');
    
    // Create cached data from this module
    const cachedData = module.createCachedData();
    
    // Create a new module using the cached data. The code must be the same.
    const module2 = new vm.SourceTextModule('const a = 1;', { cachedData });
    

    sourceTextModule.dependencySpecifiers#

    稳定性:0 - 已弃用:请改为使用 sourceTextModule.moduleRequests

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

    对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)[[RequestedModules]] 字段。

    sourceTextModule.hasAsyncGraph()#

    遍历依赖关系图,如果其依赖项中的任何模块或该模块本身包含顶级 await 表达式,则返回 true,否则返回 false

    如果图足够大,搜索速度可能会很慢。

    这要求模块首先被实例化。如果模块尚未实例化,将抛出错误。

    sourceTextModule.hasTopLevelAwait()#

    返回模块本身是否包含任何顶级 await 表达式。

    这对应于 ECMAScript 规范中循环模块记录(Cyclic Module Record)中的 [[HasTLA]] 字段。

    sourceTextModule.instantiate()#

    使用链接的请求模块实例化模块。

    这会解析模块的导入绑定,包括重新导出的绑定名称。当有任何绑定无法解析时,将同步抛出错误。

    如果请求的模块包含循环依赖项,则必须在调用此方法之前对周期中的所有模块调用 sourceTextModule.linkRequests(modules) 方法。

    sourceTextModule.linkRequests(modules)#

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

    modules 数组中模块实例的顺序应对应于 sourceTextModule.moduleRequests 被解析的顺序。如果两个模块请求具有相同的说明符和导入属性,则必须使用相同的模块实例来解析它们,否则将抛出 ERR_MODULE_LINK_MISMATCH。例如,当链接此模块的请求时:

    import foo from 'foo';
    import source Foo from 'foo';
    

    modules 数组必须包含对同一实例的两个引用,因为这两个模块请求相同但处于两个阶段。

    如果模块没有依赖项,modules 数组可以为空。

    用户可以使用 sourceTextModule.moduleRequests 来实现 ECMAScript 规范中宿主定义的 HostLoadImportedModule 抽象操作,并使用 sourceTextModule.linkRequests() 来对所有依赖项的模块批量调用规范定义的 FinishLoadingImportedModule

    决定依赖项的解析是同步还是异步取决于 SourceTextModule 的创建者。

    modules 数组中的每个模块链接后,调用 sourceTextModule.instantiate()

    sourceTextModule.moduleRequests#

    此模块请求的导入依赖项。返回的数组已被冻结,不允许对其进行任何更改。

    例如,给定一段源代码:

    import foo from 'foo';
    import fooAlias from 'foo';
    import bar from './bar.js';
    import withAttrs from '../with-attrs.ts' with { arbitraryAttr: 'attr-val' };
    import source Module from 'wasm-mod.wasm';
    

    sourceTextModule.moduleRequests 的值将为:

    [
      {
        specifier: 'foo',
        attributes: {},
        phase: 'evaluation',
      },
      {
        specifier: 'foo',
        attributes: {},
        phase: 'evaluation',
      },
      {
        specifier: './bar.js',
        attributes: {},
        phase: 'evaluation',
      },
      {
        specifier: '../with-attrs.ts',
        attributes: { arbitraryAttr: 'attr-val' },
        phase: 'evaluation',
      },
      {
        specifier: 'wasm-mod.wasm',
        attributes: {},
        phase: 'source',
      },
    ];
    

    类:vm.SyntheticModule#

    稳定性:1 - 实验性

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

    vm.SyntheticModule 类提供 WebIDL 规范中定义的合成模块记录(Synthetic Module Record)。合成模块的目的是提供一个通用接口,将非 JavaScript 资源暴露给 ECMAScript 模块图。

    import { SyntheticModule } from 'node:vm';
    
    const source = '{ "a": 1 }';
    const syntheticModule = new SyntheticModule(['default'], function() {
      const obj = JSON.parse(source);
      this.setExport('default', obj);
    });
    
    // Use `syntheticModule` in linking
    (async () => {
      await syntheticModule.link(() => {});
      await syntheticModule.evaluate();
    
      console.log('Default export:', syntheticModule.namespace.default);
    })();
    const { SyntheticModule } = require('node:vm');
    
    const source = '{ "a": 1 }';
    const syntheticModule = new SyntheticModule(['default'], function() {
      const obj = JSON.parse(source);
      this.setExport('default', obj);
    });
    
    // Use `syntheticModule` in linking
    (async () => {
      await syntheticModule.link(() => {});
      await syntheticModule.evaluate();
    
      console.log('Default export:', syntheticModule.namespace.default);
    })();
    

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

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

    创建新的 SyntheticModule 实例。

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

    syntheticModule.setExport(name, value)#

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

    此方法使用给定值设置模块导出绑定槽。

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

    类型:ModuleRequest#

    ModuleRequest 表示带有给定导入属性和阶段的模块导入请求。

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

    • code <string> 要编译的函数主体。
    • params <string[]> 包含函数所有参数的字符串数组。
    • options <Object>
    • filename <string> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: ''
    • lineOffset <number> 指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
    • columnOffset <number> 指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
    • cachedData <Buffer> | <TypedArray> | <DataView> 为提供的源提供可选的 BufferTypedArrayDataView,其中包含 V8 的代码缓存数据。这必须由先前使用相同 codeparamsvm.compileFunction() 调用生成。
    • produceCachedData <boolean> 指定是否生成新的缓存数据。默认值: false
    • parsingContext <Object> 应在其中编译所述函数的已上下文化的对象。
    • contextExtensions <Object[]> 一个包含编译时要应用的一组上下文扩展(包装当前作用域的对象)的数组。默认值: []
    • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在此函数评估期间调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态 import() 的支持
  • 返回:<Function>
  • 将给定的代码编译到提供的上下文中(如果未提供上下文,则使用当前上下文),并将其包装在具有给定 params 的函数中返回。

    vm.constants#

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

    vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER#

    稳定性:1.1 - 活跃开发中

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

    详细信息请参阅编译 API 对动态 import() 的支持

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

    • contextObject <Object> | <vm.constants.DONT_CONTEXTIFY> | <undefined> 要么是 vm.constants.DONT_CONTEXTIFY,要么是一个将被上下文化的对象。如果为 undefined,将创建一个空的已上下文化对象以实现向后兼容。
    • options <Object>
    • name <string> 新创建上下文的可读名称。默认值: 'VM Context i',其中 i 是创建上下文的递增数字索引。
    • origin <string> 用于显示目的、与新创建上下文相对应的源(Origin)。源应格式化为 URL,但仅包含方案、主机和端口(如有必要),如 URL 对象的 url.origin 属性值。最重要的是,此字符串应省略尾随斜杠,因为它表示路径。默认值: ''
    • 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 模块运行的脚本之外,全局变量将保持不变。

    import { createContext, runInContext } from 'node:vm';
    
    global.globalVar = 3;
    
    const context = { globalVar: 1 };
    createContext(context);
    
    runInContext('globalVar *= 2;', context);
    
    console.log(context);
    // Prints: { globalVar: 2 }
    
    console.log(global.globalVar);
    // Prints: 3
    const { createContext, runInContext } = require('node:vm');
    
    global.globalVar = 3;
    
    const context = { globalVar: 1 };
    createContext(context);
    
    runInContext('globalVar *= 2;', context);
    
    console.log(context);
    // Prints: { globalVar: 2 }
    
    console.log(global.globalVar);
    // Prints: 3
    

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

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

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

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

  • vm.isContext(object)#

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

    vm.measureMemory([options])#

    稳定性:1 - 实验性

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

    • 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 实例中每个堆空间占用的内存。

    import { createContext, measureMemory } from 'node:vm';
    // Measure the memory used by the main context.
    measureMemory({ mode: 'summary' })
      // This is the same as vm.measureMemory()
      .then((result) => {
        // The current format is:
        // {
        //   total: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] },
        //   WebAssembly: { code: 0, metadata: 33962 },
        // }
        console.log(result);
      });
    
    const context = createContext({ a: 1 });
    measureMemory({ mode: 'detailed', execution: 'eager' }).then((result) => {
      // Reference the context here so that it won't be GC'ed
      // until the measurement is complete.
      console.log('Context:', context.a);
      // {
      //   total: { jsMemoryEstimate: 1767100, jsMemoryRange: [1767100, 5440560] },
      //   WebAssembly: { code: 0, metadata: 33962 },
      //   current: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] },
      //   other: [{ jsMemoryEstimate: 165272, jsMemoryRange: [Array] }],
      // }
      console.log(result);
    });
    const { createContext, measureMemory } = require('node:vm');
    // Measure the memory used by the main context.
    measureMemory({ mode: 'summary' })
      // This is the same as vm.measureMemory()
      .then((result) => {
        // The current format is:
        // {
        //   total: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] },
        //   WebAssembly: { code: 0, metadata: 33962 },
        // }
        console.log(result);
      });
    
    const context = createContext({ a: 1 });
    measureMemory({ mode: 'detailed', execution: 'eager' }).then((result) => {
      // Reference the context here so that it won't be GC'ed
      // until the measurement is complete.
      console.log('Context:', context.a);
      // {
      //   total: { jsMemoryEstimate: 1767100, jsMemoryRange: [1767100, 5440560] },
      //   WebAssembly: { code: 0, metadata: 33962 },
      //   current: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] },
      //   other: [{ jsMemoryEstimate: 165272, jsMemoryRange: [Array] }],
      // }
      console.log(result);
    });
    
  • vm.runInContext(code, contextifiedObject[, options])#

    • code <string> 要编译和运行的 JavaScript 代码。
    • contextifiedObject <Object>code 被编译和运行时,将被用作 global 的已上下文化对象。
    • options <Object> | <string>
      • filename <string> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.<anonymous>'
      • lineOffset <number> 指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
      • columnOffset <number> 指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
      • displayErrors <boolean> 当为 true 时,如果编译 code 时发生 Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值: true
      • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出 Error。该值必须是严格的正整数。
      • breakOnSigint <boolean> 如果为 true,接收 SIGINT (Ctrl+C) 将终止执行并抛出 Error。通过 process.on('SIGINT') 附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值: false
      • cachedData <Buffer> | <TypedArray> | <DataView> 为提供的源提供可选的 BufferTypedArrayDataView,其中包含 V8 的代码缓存数据。
      • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在此脚本评估期间调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态 import() 的支持

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

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

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

    import { createContext, runInContext } from 'node:vm';
    
    const contextObject = { globalVar: 1 };
    createContext(contextObject);
    
    for (let i = 0; i < 10; ++i) {
      runInContext('globalVar *= 2;', contextObject);
    }
    console.log(contextObject);
    // Prints: { globalVar: 1024 }
    const { createContext, runInContext } = require('node:vm');
    
    const contextObject = { globalVar: 1 };
    createContext(contextObject);
    
    for (let i = 0; i < 10; ++i) {
      runInContext('globalVar *= 2;', contextObject);
    }
    console.log(contextObject);
    // Prints: { globalVar: 1024 }
    

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

    • code <string> 要编译和运行的 JavaScript 代码。
    • contextObject <Object> | <vm.constants.DONT_CONTEXTIFY> | <undefined> 要么是 vm.constants.DONT_CONTEXTIFY,要么是一个将被上下文化的对象。如果为 undefined,将创建一个空的已上下文化对象以实现向后兼容。
    • options <Object> | <string>
      • filename <string> 指定此脚本生成的堆栈跟踪中使用的文件名。默认值: 'evalmachine.<anonymous>'
      • lineOffset <number> 指定此脚本生成的堆栈跟踪中显示的行号偏移量。默认值: 0
      • columnOffset <number> 指定此脚本生成的堆栈跟踪中显示的第一行列号偏移量。默认值: 0
      • displayErrors <boolean> 当为 true 时,如果编译 code 时发生 Error,则导致错误的这一行代码将被附加到堆栈跟踪中。默认值: true
      • timeout <integer> 指定在终止执行前执行 code 的毫秒数。如果执行被终止,将抛出 Error。该值必须是严格的正整数。
      • breakOnSigint <boolean> 如果为 true,接收 SIGINT (Ctrl+C) 将终止执行并抛出 Error。通过 process.on('SIGINT') 附加的现有事件处理程序在脚本执行期间会被禁用,但在此之后会继续生效。默认值: false
      • contextName <string> 新创建上下文的可读名称。默认值: 'VM Context i',其中 i 是创建上下文的递增数字索引。
      • contextOrigin <string> 用于显示目的、与新创建上下文相对应的源(Origin)。源应格式化为 URL,但仅包含方案、主机和端口(如有必要),如 URL 对象的 url.origin 属性值。最重要的是,此字符串应省略尾随斜杠,因为它表示路径。默认值: ''
      • contextCodeGeneration <Object>
        • strings <boolean> 如果设为 false,任何对 eval 或函数构造函数(FunctionGeneratorFunction 等)的调用都将抛出 EvalError默认值: true
        • wasm <boolean> 如果设为 false,任何编译 WebAssembly 模块的尝试都将抛出 WebAssembly.CompileError默认值: true
      • cachedData <Buffer> | <TypedArray> | <DataView> 为提供的源提供可选的 BufferTypedArrayDataView,其中包含 V8 的代码缓存数据。
      • importModuleDynamically <Function> | <vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER> 用于指定在此脚本评估期间调用 import() 时应如何加载模块。此选项是实验性模块 API 的一部分。我们不建议在生产环境中使用它。详细信息请参阅编译 API 对动态 import() 的支持
      • microtaskMode <string> 如果设为 afterEvaluate,微任务(通过 Promiseasync function 调度的任务)将在脚本运行后立即运行。在这种情况下,它们会被包含在 timeoutbreakOnSigint 的作用域中。
    • 返回:<any> 脚本中执行的最后一条语句的结果。

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

    它同时执行多项操作:

    1. 创建一个新上下文。
    2. 如果 contextObject 是对象,则使用新上下文将其上下文化。如果 contextObject 未定义,则创建一个新对象并将其上下文化。如果 contextObjectvm.constants.DONT_CONTEXTIFY,则不上下文化任何东西。
    3. 将代码编译为 vm.Script
    4. 在创建的上下文中运行已编译的代码。代码无法访问调用此方法的作用域。
    5. 返回结果。

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

    import { runInNewContext, constants } from 'node:vm';
    
    const contextObject = {
      animal: 'cat',
      count: 2,
    };
    
    runInNewContext('count += 1; name = "kitty"', contextObject);
    console.log(contextObject);
    // Prints: { animal: 'cat', count: 3, name: 'kitty' }
    
    // This would throw if the context is created from a contextified object.
    // vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that
    // can be frozen.
    const frozenContext = runInNewContext(
      'Object.freeze(globalThis); globalThis;',
      constants.DONT_CONTEXTIFY,
    );
    const { runInNewContext, constants } = require('node:vm');
    
    const contextObject = {
      animal: 'cat',
      count: 2,
    };
    
    runInNewContext('count += 1; name = "kitty"', contextObject);
    console.log(contextObject);
    // Prints: { animal: 'cat', count: 3, name: 'kitty' }
    
    // This would throw if the context is created from a contextified object.
    // vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that
    // can be frozen.
    const frozenContext = runInNewContext(
      'Object.freeze(globalThis); globalThis;',
      constants.DONT_CONTEXTIFY,
    );
    

    vm.runInThisContext(code[, options])#

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

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

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

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

    import { runInThisContext } from 'node:vm';
    let localVar = 'initial value';
    
    const vmResult = runInThisContext('localVar = "vm";');
    console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`);
    // Prints: vmResult: 'vm', localVar: 'initial value'
    
    const evalResult = eval('localVar = "eval";');
    console.log(`evalResult: '${evalResult}', localVar: '${localVar}'`);
    // Prints: evalResult: 'eval', localVar: 'eval'
    const { runInThisContext } = require('node:vm');
    let localVar = 'initial value';
    
    const vmResult = runInThisContext('localVar = "vm";');
    console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`);
    // Prints: vmResult: 'vm', localVar: 'initial value'
    
    const evalResult = eval('localVar = "eval";');
    console.log(`evalResult: '${evalResult}', localVar: '${localVar}'`);
    // Prints: evalResult: 'eval', localVar: 'eval'
    

    因为 vm.runInThisContext() 无法访问本地作用域,所以 localVar 未被更改。相反,直接 eval() 调用确实可以访问本地作用域,因此 localVar 的值被更改了。通过这种方式,vm.runInThisContext() 非常类似于一个间接 eval() 调用,例如 (0,eval)('code')

    示例:在虚拟机(VM)内运行 HTTP 服务器#

    当使用 script.runInThisContext()vm.runInThisContext() 时,代码是在当前 V8 全局上下文中执行的。传递给此 VM 上下文的代码将拥有其自己的独立作用域。

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

    import { runInThisContext } from 'node:vm';
    import { createRequire } from 'node:module';
    
    const require = createRequire(import.meta.url);
    
    const code = `
    ((require) => {
      const { createServer } = require('node: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/');
    })`;
    
    runInThisContext(code)(require);
    const { runInThisContext } = require('node:vm');
    
    const code = `
    ((require) => {
      const { createServer } = require('node: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/');
    })`;
    
    runInThisContext(code)(require);
    

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

    “上下文化”(contextify)一个对象是什么意思?#

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

    在 V8 中,上下文是一个执行环境,允许独立的、不相关的 JavaScript 应用程序在单个 V8 实例中运行。您必须显式指定要在其中运行任何 JavaScript 代码的上下文。

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

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

    import { createContext, runInContext } from 'node:vm';
    
    // An undefined `contextObject` option makes the global object contextified.
    const context = createContext();
    console.log(runInContext('globalThis', context) === context);  // false
    // A contextified global object cannot be frozen.
    try {
      runInContext('Object.freeze(globalThis);', context);
    } catch (e) {
      console.log(`${e.constructor.name}: ${e.message}`); // TypeError: Cannot freeze
    }
    console.log(runInContext('globalThis.foo = 1; foo;', context));  // 1
    const { createContext, runInContext } = require('node:vm');
    
    // An undefined `contextObject` option makes the global object contextified.
    const context = createContext();
    console.log(runInContext('globalThis', context) === context);  // false
    // A contextified global object cannot be frozen.
    try {
      runInContext('Object.freeze(globalThis);', context);
    } catch (e) {
      console.log(`${e.constructor.name}: ${e.message}`); // TypeError: Cannot freeze
    }
    console.log(runInContext('globalThis.foo = 1; foo;', context));  // 1
    

    要创建一个具有普通全局对象的上下文,并以较少的怪癖在外部上下文中访问全局代理,请指定 vm.constants.DONT_CONTEXTIFY 作为 contextObject 参数。

    vm.constants.DONT_CONTEXTIFY#

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

    import { createContext, runInContext, constants } from 'node:vm';
    
    // Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
    const context = createContext(constants.DONT_CONTEXTIFY);
    runInContext('Object.freeze(globalThis);', context);
    try {
      runInContext('bar = 1; bar;', context);
    } catch (e) {
      console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: bar is not defined
    }
    const { createContext, runInContext, constants } = require('node:vm');
    
    // Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
    const context = createContext(constants.DONT_CONTEXTIFY);
    runInContext('Object.freeze(globalThis);', context);
    try {
      runInContext('bar = 1; bar;', context);
    } catch (e) {
      console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: bar is not defined
    }
    

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

    import { createContext, runInContext, constants } from 'node:vm';
    
    const context = createContext(constants.DONT_CONTEXTIFY);
    
    // Returned object is reference equal to globalThis in the new context.
    console.log(runInContext('globalThis', context) === context);  // true
    
    // Can be used to access globals in the new context directly.
    console.log(context.Array);  // [Function: Array]
    runInContext('foo = 1;', context);
    console.log(context.foo);  // 1
    context.bar = 1;
    console.log(runInContext('bar;', context));  // 1
    
    // Can be frozen and it affects the inner context.
    Object.freeze(context);
    try {
      runInContext('baz = 1; baz;', context);
    } catch (e) {
      console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: baz is not defined
    }
    const { createContext, runInContext, constants } = require('node:vm');
    
    const context = createContext(constants.DONT_CONTEXTIFY);
    
    // Returned object is reference equal to globalThis in the new context.
    console.log(runInContext('globalThis', context) === context);  // true
    
    // Can be used to access globals in the new context directly.
    console.log(context.Array);  // [Function: Array]
    runInContext('foo = 1;', context);
    console.log(context.foo);  // 1
    context.bar = 1;
    console.log(runInContext('bar;', context));  // 1
    
    // Can be frozen and it affects the inner context.
    Object.freeze(context);
    try {
      runInContext('baz = 1; baz;', context);
    } catch (e) {
      console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: baz is not defined
    }
    

    超时与异步任务及 Promise 的交互#

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

    例如,以下代码由 vm.runInNewContext() 以 5 毫秒的超时时间执行,它调度了一个在 promise 解析后运行的无限循环。调度的循环永远不会被超时中断:

    import { runInNewContext } from 'node:vm';
    
    function loop() {
      console.log('entering loop');
      while (1) console.log(Date.now());
    }
    
    runInNewContext(
      'Promise.resolve().then(() => loop());',
      { loop, console },
      { timeout: 5 },
    );
    // This is printed *before* 'entering infinite loop' (!)
    console.log('done executing');
    const { runInNewContext } = require('node:vm');
    
    function loop() {
      console.log('entering loop');
      while (1) console.log(Date.now());
    }
    
    runInNewContext(
      'Promise.resolve().then(() => loop());',
      { loop, console },
      { timeout: 5 },
    );
    // This is printed *before* 'entering infinite loop' (!)
    console.log('done executing');
    

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

    import { runInNewContext } from 'node:vm';
    
    function loop() {
      while (1) console.log(Date.now());
    }
    
    runInNewContext(
      'Promise.resolve().then(() => loop());',
      { loop, console },
      { timeout: 5, microtaskMode: 'afterEvaluate' },
    );
    const { runInNewContext } = require('node:vm');
    
    function loop() {
      while (1) console.log(Date.now());
    }
    
    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() 等),传递给它们的函数将被添加到所有上下文共享的全局队列中。因此,传递给这些函数的回调也无法通过超时控制。

    microtaskMode'afterEvaluate' 时,需注意在上下文间共享 Promise#

    'afterEvaluate' 模式下,Context 拥有自己的微任务队列,与外部(主)上下文使用的全局微任务队列分开。虽然此模式对于强制执行 timeout 和启用异步任务的 breakOnSigint 是必要的,但它也使得在上下文间共享 Promise 变得具有挑战性。

    在下面的示例中,一个 Promise 在内部上下文中创建并与外部上下文共享。当外部上下文 await 该 Promise 时,外部上下文的执行流以令人惊讶的方式被中断:日志语句永远不会被执行。

    import { createContext, runInContext } from 'node:vm';
    
    const inner_context = createContext({}, { microtaskMode: 'afterEvaluate' });
    
    // runInContext() returns a Promise created in the inner context.
    const inner_promise = runInContext('Promise.resolve()', inner_context);
    
    // As part of performing `await`, the JavaScript runtime must enqueue a task
    // on the microtask queue of the context where `inner_promise` was created.
    // A task is added on the inner microtask queue, but **it will not be run
    // automatically**: this task will remain pending indefinitely.
    //
    // Since the outer microtask queue is empty, execution in the outer module
    // falls through, and the log statement below is never executed.
    await inner_promise;
    
    console.log('this will NOT be printed');
    const { createContext, runInContext } = require('node:vm');
    
    // runInContext() returns a Promise created in the inner context.
    const inner_context = createContext({}, { microtaskMode: 'afterEvaluate' });
    
    (async () => {
      const inner_promise = runInContext('Promise.resolve()', inner_context);
    
      // As part of performing `await`, the JavaScript runtime must enqueue a task
      // on the microtask queue of the context where `inner_promise` was created.
      // A task is added on the inner microtask queue, but **it will not be run
      // automatically**: this task will remain pending indefinitely.
      //
      // Since the outer microtask queue is empty, execution in the outer module
      // falls through, and the log statement below is never executed.
      await inner_promise;
    
      console.log('this will NOT be printed');
    })();
    

    要成功地在具有不同微任务队列的上下文之间共享 Promise,有必要确保无论何时外部上下文在内部微任务队列上调度任务,内部微任务队列上的任务都将运行。

    只要在脚本或模块上调用 runInContext()SourceTextModule.evaluate() 时,给定上下文的微任务队列上的任务就会运行。在我们的示例中,可以通过在 await inner_promise 之前调度第二次 runInContext() 调用来恢复正常的执行流。

    // Schedule `runInContext()` to manually drain the inner context microtask
    // queue; it will run after the `await` statement below.
    setImmediate(() => {
      vm.runInContext('', context);
    });
    
    await inner_promise;
    
    console.log('OK');
    

    注意: 严格来说,在这种模式下,node:vm 背离了 ECMAScript 规范中关于排队作业(enqueing jobs)的文字规定,允许来自不同上下文的异步任务以不同于它们被排队的顺序运行。

    编译 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 时#

    如果未指定此选项,或者其值为 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 实例。

    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 loaded from the main context is not an instance of the Function
    // class in the new context.
    script.runInNewContext().then(console.log);
    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 loaded from the main context is not an instance of the Function
    // class in the new context.
    script.runInNewContext().then(console.log);
    

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

    import { Script, constants } from 'node:vm';
    import { resolve } from 'node:path';
    import { writeFileSync } from 'node:fs';
    
    // Write test.js and test.txt to the directory where the current script
    // being run is located.
    writeFileSync(resolve(import.meta.dirname, 'test.mjs'),
                  'export const filename = "./test.json";');
    writeFileSync(resolve(import.meta.dirname, 'test.json'),
                  '{"hello": "world"}');
    
    // Compile a script that loads test.mjs and then test.json
    // as if the script is placed in the same directory.
    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);
    const { Script, constants } = require('node:vm');
    const { resolve } = require('node:path');
    const { writeFileSync } = require('node:fs');
    
    // Write test.js and test.txt to the directory where the current script
    // being run is located.
    writeFileSync(resolve(__dirname, 'test.mjs'),
                  'export const filename = "./test.json";');
    writeFileSync(resolve(__dirname, 'test.json'),
                  '{"hello": "world"}');
    
    // Compile a script that loads test.mjs and then test.json
    // as if the script is placed in the same directory.
    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);
    

    使用来自主上下文的默认加载器加载用户模块有几个注意事项:

    1. 正在解析的模块将相对于传递给 vm.Scriptvm.compileFunction()filename 选项。解析可以适用于作为绝对路径或 URL 字符串的 filename。如果 filename 是一个既不是绝对路径也不是 URL 的字符串,或者它是 undefined,解析将相对于进程的当前工作目录。对于 vm.createContext(),解析始终相对于当前工作目录,因为此选项仅在没有引用者脚本或模块时使用。
    2. 对于解析为特定路径的任何给定 filename,一旦进程成功从该路径加载特定模块,结果可能会被缓存,并且后续从同一路径加载同一模块将返回相同的内容。如果 filename 是 URL 字符串,如果它具有不同的搜索参数,则不会命中缓存。对于非 URL 字符串的 filename,目前没有办法绕过缓存行为。

    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> 对于 new vm.Scriptvm.runInThisContextvm.runInContextvm.runInNewContext,referrer 是已编译的 vm.Script。对于 vm.compileFunction,它是已编译的 Function;对于 new vm.SourceTextModule,它是已编译的 vm.SourceTextModule;对于 vm.createContext(),它是上下文 Object
    • importAttributes <Object> 传递给 optionsExpression 可选参数的 "with" 值,如果未提供值,则为空对象。
    • phase <string> 动态导入的阶段("source""evaluation")。
    • 返回: <Module Namespace Object> | <vm.Module> 建议返回 vm.Module,以便利用错误跟踪功能,并避免包含 then 函数导出的命名空间所带来的问题。
    // This script must be run with --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);   // The compiled script
        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' } }
    // This script must be run with --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);   // The compiled script
          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' } }
    })();