import()

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2020 年 1 月⁩ 起,所有主流浏览器均已支持。

* 此特性的某些部分可能存在不同级别的支持。

import() 语法,通常称为*动态导入*,是一个类似函数的表达式,允许异步和动态地将一个 ECMAScript 模块加载到一个可能非模块的环境中。

声明式导入不同,动态导入只在需要时才被评估,并允许更大的语法灵活性。

语法

js
import(moduleName)
import(moduleName, options)

import() 调用是一种非常类似于函数调用的语法,但 import 本身是一个关键字,而不是一个函数。你不能像 const myImport = import 那样给它起别名,这样做会抛出一个 SyntaxError

只有当运行时也支持 options 参数时,才允许使用尾随逗号。请检查浏览器兼容性

参数

moduleName

要导入的模块。模块说明符的评估由宿主环境指定,但总是遵循与静态 import 声明相同的算法。

options

一个包含导入选项的对象。识别以下键

with

导入属性

返回值

返回一个 promise,它将

  • 如果引用的模块成功加载和评估,则兑现(fulfills)为一个模块命名空间对象:一个包含 moduleName 中所有导出的对象。
  • 如果 moduleName字符串强制转换抛出错误,则拒绝(rejects)并返回抛出的错误。
  • 如果模块的获取和加载因任何原因失败,则拒绝并返回一个由实现定义的错误(Node 使用一个通用的 Error,而所有浏览器都使用 TypeError)。常见原因可能包括
    • 在基于文件系统的模块系统中(例如 Node.js),如果访问文件系统失败(权限被拒绝、文件未找到等)。
    • 在基于 Web 的模块系统中(例如浏览器),如果网络请求失败(未连接到互联网、CORS 问题等)或发生 HTTP 错误(404、500 等)。
  • 如果引用模块的评估抛出错误,则拒绝并返回抛出的错误。

注意: import() 绝不会同步抛出错误。

描述

import 声明语法(import something from "somewhere")是静态的,并且总是会导致被导入的模块在加载时被评估。动态导入允许人们规避 import 声明的语法僵化,并根据条件或按需加载模块。以下是你可能需要使用动态导入的一些原因

  • 当静态导入显著减慢代码加载速度或增加程序内存使用,并且你需要导入的代码被使用的可能性很低,或者你直到稍后才需要它时。
  • 当你导入的模块在加载时不存在时。
  • 当导入说明符字符串需要动态构建时。(静态导入只支持静态说明符。)
  • 当被导入的模块有副作用,并且你只希望在某个条件为真时才产生这些副作用时。(建议模块中不要有任何副作用,但有时你无法控制模块依赖项中的这种情况。)
  • 当你处于非模块环境中时(例如,eval 或脚本文件)。

仅在必要时使用动态导入。静态形式更适合加载初始依赖项,并且可以更容易地从静态分析工具和摇树优化中受益。

如果你的文件不作为模块运行(如果它在 HTML 文件中被引用,script 标签必须有 type="module"),你将无法使用静态导入声明。另一方面,异步的动态导入语法始终可用,允许你将模块导入到非模块环境中。

options 参数允许不同类型的导入选项。例如,导入属性

js
import("./data.json", { with: { type: "json" } });

并非所有执行上下文都允许动态模块导入。例如,import() 可以在主线程、共享工作线程或专用工作线程中使用,但如果在 service workerworklet 中调用,则会抛出错误。

模块命名空间对象

模块命名空间对象是一个描述模块所有导出的对象。它是在模块评估时创建的静态对象。有两种方式可以访问一个模块的命名空间对象:通过命名空间导入import * as name from moduleName),或者通过动态导入的兑现值。

模块命名空间对象是一个原型为 null密封对象。这意味着该对象的所有字符串键都对应于模块的导出,并且永远不会有额外的键。所有键都按字典序可枚举(即 Array.prototype.sort() 的默认行为),默认导出可作为一个名为 default 的键来访问。此外,模块命名空间对象有一个 [Symbol.toStringTag] 属性,其值为 "Module",用于 Object.prototype.toString()

当你使用 Object.getOwnPropertyDescriptors() 获取其描述符时,字符串属性是不可配置且可写的。然而,它们实际上是只读的,因为你不能给属性重新赋一个新值。这种行为反映了静态导入创建“实时绑定”的事实——这些值可以由导出它们的模块重新赋值,但不能由导入它们的模块重新赋值。属性的可写性反映了值可能发生变化的可能性,因为不可配置且不可写的属性必须是常量。例如,你可以重新赋值一个导出变量的值,并且新值可以在模块命名空间对象中观察到。

每个(规范化的)模块说明符对应一个唯一的模块命名空间对象,所以以下通常为真

js
import * as mod from "/my-module.js";

import("/my-module.js").then((mod2) => {
  console.log(mod === mod2); // true
});

除了一个奇特的情况:因为 promise 永远不会兑现为一个 thenable,如果 my-module.js 模块导出了一个名为 then() 的函数,那么当动态导入的 promise 被兑现时,该函数将作为 promise 解析过程的一部分被自动调用。

js
// my-module.js
export function then(resolve) {
  console.log("then() called");
  resolve(1);
}
js
// main.js
import * as mod from "/my-module.js";

import("/my-module.js").then((mod2) => {
  // Logs "then() called"
  console.log(mod === mod2); // false
});

警告: 不要从模块中导出名为 then() 的函数。这会导致模块在动态导入时的行为与静态导入时不同。

这种积极的缓存确保了一段 JavaScript 代码永远不会被执行超过一次,即使它被多次导入。未来的导入甚至不会导致 HTTP 请求或磁盘访问。如果你确实需要重新导入并重新评估一个模块而不重启整个 JavaScript 环境,一个可能的技巧是在模块说明符中使用一个唯一的查询参数。这在支持 URL 说明符的非浏览器运行时中也有效。

js
import(`/my-module.js?t=${Date.now()}`);

请注意,在一个长期运行的应用程序中,这可能导致内存泄漏,因为引擎无法安全地垃圾回收任何模块命名空间对象。目前,没有办法手动清除模块命名空间对象的缓存。

模块命名空间对象缓存仅适用于成功加载和链接的模块。一个模块的导入分三步:加载(获取模块)、链接(主要是解析模块)和评估(执行解析后的代码)。只有评估失败会被缓存;如果一个模块加载或链接失败,下一次导入可能会再次尝试加载和链接该模块。浏览器可能会也可能不会缓存获取操作的结果,但它应该遵循典型的 HTTP 语义,因此处理这类网络故障与处理 fetch() 故障没有区别。

示例

仅为副作用导入模块

js
(async () => {
  if (somethingIsTrue) {
    // import module for side effects
    await import("/modules/my-module.js");
  }
})();

如果你的项目使用导出 ESM 的包,你也可以只为副作用导入它们。这只会运行包入口点文件(以及它导入的任何文件)中的代码。

导入默认值

如果你要解构导入的模块命名空间对象,那么你必须重命名 default 键,因为 default 是一个保留字。

js
(async () => {
  if (somethingIsTrue) {
    const {
      default: myDefault,
      foo,
      bar,
    } = await import("/modules/my-module.js");
  }
})();

响应用户操作按需导入

这个例子展示了如何根据用户操作(本例中为按钮点击)将功能加载到页面上,然后调用该模块中的一个函数。这不是实现此功能的唯一方法。import() 函数也支持 await

js
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
  link.addEventListener("click", (e) => {
    e.preventDefault();

    import("/modules/my-module.js")
      .then((module) => {
        module.loadPageInto(main);
      })
      .catch((err) => {
        main.textContent = err.message;
      });
  });
}

根据环境导入不同模块

在诸如服务器端渲染之类的过程中,你可能需要在服务器或浏览器上加载不同的逻辑,因为它们与不同的全局变量或模块交互(例如,浏览器代码可以访问像 documentnavigator 这样的 Web API,而服务器代码可以访问服务器文件系统)。你可以通过条件动态导入来做到这一点。

js
let myModule;

if (typeof window === "undefined") {
  myModule = await import("module-used-on-server");
} else {
  myModule = await import("module-used-in-browser");
}

使用非字面量说明符导入模块

动态导入允许任何表达式作为模块说明符,而不必是字符串字面量。

在这里,我们并发加载 10 个模块,/modules/module-0.js/modules/module-1.js 等,并调用每个模块导出的 load 函数。

js
Promise.all(
  Array.from({ length: 10 }).map(
    (_, index) => import(`/modules/module-${index}.js`),
  ),
).then((modules) => modules.forEach((module) => module.load()));

规范

规范
ECMAScript® 2026 语言规范
# sec-import-calls

浏览器兼容性

另见