import()
Baseline 广泛可用 *
import() 语法,通常称为*动态导入*,是一个类似函数的表达式,允许异步和动态地将一个 ECMAScript 模块加载到一个可能非模块的环境中。
与声明式导入不同,动态导入只在需要时才被评估,并允许更大的语法灵活性。
语法
import(moduleName)
import(moduleName, options)
import() 调用是一种非常类似于函数调用的语法,但 import 本身是一个关键字,而不是一个函数。你不能像 const myImport = import 那样给它起别名,这样做会抛出一个 SyntaxError。
参数
moduleName-
要导入的模块。模块说明符的评估由宿主环境指定,但总是遵循与静态 import 声明相同的算法。
options-
一个包含导入选项的对象。识别以下键
返回值
返回一个 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 参数允许不同类型的导入选项。例如,导入属性
import("./data.json", { with: { type: "json" } });
并非所有执行上下文都允许动态模块导入。例如,import() 可以在主线程、共享工作线程或专用工作线程中使用,但如果在 service worker 或 worklet 中调用,则会抛出错误。
模块命名空间对象
模块命名空间对象是一个描述模块所有导出的对象。它是在模块评估时创建的静态对象。有两种方式可以访问一个模块的命名空间对象:通过命名空间导入(import * as name from moduleName),或者通过动态导入的兑现值。
模块命名空间对象是一个原型为 null 的密封对象。这意味着该对象的所有字符串键都对应于模块的导出,并且永远不会有额外的键。所有键都按字典序可枚举(即 Array.prototype.sort() 的默认行为),默认导出可作为一个名为 default 的键来访问。此外,模块命名空间对象有一个 [Symbol.toStringTag] 属性,其值为 "Module",用于 Object.prototype.toString()。
当你使用 Object.getOwnPropertyDescriptors() 获取其描述符时,字符串属性是不可配置且可写的。然而,它们实际上是只读的,因为你不能给属性重新赋一个新值。这种行为反映了静态导入创建“实时绑定”的事实——这些值可以由导出它们的模块重新赋值,但不能由导入它们的模块重新赋值。属性的可写性反映了值可能发生变化的可能性,因为不可配置且不可写的属性必须是常量。例如,你可以重新赋值一个导出变量的值,并且新值可以在模块命名空间对象中观察到。
每个(规范化的)模块说明符对应一个唯一的模块命名空间对象,所以以下通常为真
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 解析过程的一部分被自动调用。
// my-module.js
export function then(resolve) {
console.log("then() called");
resolve(1);
}
// 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 说明符的非浏览器运行时中也有效。
import(`/my-module.js?t=${Date.now()}`);
请注意,在一个长期运行的应用程序中,这可能导致内存泄漏,因为引擎无法安全地垃圾回收任何模块命名空间对象。目前,没有办法手动清除模块命名空间对象的缓存。
模块命名空间对象缓存仅适用于成功加载和链接的模块。一个模块的导入分三步:加载(获取模块)、链接(主要是解析模块)和评估(执行解析后的代码)。只有评估失败会被缓存;如果一个模块加载或链接失败,下一次导入可能会再次尝试加载和链接该模块。浏览器可能会也可能不会缓存获取操作的结果,但它应该遵循典型的 HTTP 语义,因此处理这类网络故障与处理 fetch() 故障没有区别。
示例
仅为副作用导入模块
(async () => {
if (somethingIsTrue) {
// import module for side effects
await import("/modules/my-module.js");
}
})();
如果你的项目使用导出 ESM 的包,你也可以只为副作用导入它们。这只会运行包入口点文件(以及它导入的任何文件)中的代码。
导入默认值
如果你要解构导入的模块命名空间对象,那么你必须重命名 default 键,因为 default 是一个保留字。
(async () => {
if (somethingIsTrue) {
const {
default: myDefault,
foo,
bar,
} = await import("/modules/my-module.js");
}
})();
响应用户操作按需导入
这个例子展示了如何根据用户操作(本例中为按钮点击)将功能加载到页面上,然后调用该模块中的一个函数。这不是实现此功能的唯一方法。import() 函数也支持 await。
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;
});
});
}
根据环境导入不同模块
在诸如服务器端渲染之类的过程中,你可能需要在服务器或浏览器上加载不同的逻辑,因为它们与不同的全局变量或模块交互(例如,浏览器代码可以访问像 document 和 navigator 这样的 Web API,而服务器代码可以访问服务器文件系统)。你可以通过条件动态导入来做到这一点。
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 函数。
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 |
浏览器兼容性
加载中…