先记住这个答案
静态 import 是模块声明,只能直接写在模块的顶层。把它放进 if、函数或循环,代码在解析阶段就会抛 SyntaxError,即使那个分支永远不会执行。因为模块加载器要先建立依赖关系,再执行模块;条件判断则要到执行时才能得到结果。真正需要条件加载,用动态 import(),它是表达式,返回 Promise。
- 静态
import只能直接写在模块顶层 - 块内静态导入会抛
SyntaxError - 动态
import()可在运行时按条件加载 - 动态导入的结果是模块命名空间对象
条件加载该怎么写
比如管理后台只在用户打开报表页时才需要图表库。静态声明不能用一个 if 包起来,否则连页面入口模块都无法解析。动态导入把加载动作放到实际进入分支时,适合这种按需场景。
async function openReport(shouldLoad) {
if (!shouldLoad) return
const charts = await import('./charts.js') // 进入分支才加载
charts.renderReport()
}这里的 charts 是模块命名空间对象,里面有导出的成员。import() 返回 Promise,因此可以配合 await;如果加载失败,Promise 会拒绝,调用方要处理错误。静态导入则会在模块执行前进入依赖图,即使把使用它的代码放在条件分支里,也不会变成按条件加载。
解析阶段就会报错
常见误解是“把导入放在不会执行的分支里就安全”。import 声明的语法位置在解析时检查,运行时条件根本没有机会参与。下面的代码不能作为模块运行,错误不是因为找不到文件,而是声明出现在了代码块中。
// 错误:即使条件为 false,也会抛 SyntaxError
if (false) {
import { renderReport } from './charts.js'
}
// 合法:直接位于模块顶层
import { renderReport } from './charts.js'“顶层”指不在函数、类或代码块内部,不要求写在文件的第一行。静态声明可以位于其他顶层语句之后;加载器仍会在执行模块正文前处理它。把静态导入当成普通的函数调用来读,就容易把这个时间顺序说反。
静态与动态导入的取舍
页面一启动就必须用到的依赖,写静态 import 更直接,也便于工具在构建时分析依赖。只有用户触发操作时才用到的大型模块,可以考虑动态 import()。但“动态”不保证一定减少下载量,具体还取决于打包器是否拆包、浏览器缓存以及该模块是否已经被其他地方静态引入。
动态导入不是把语法限制绕开后就能随意拼路径。运行时提供的模块说明符仍需能被当前环境解析;构建工具对变量路径通常也有自己的限制。代码里先用固定路径确认加载行为,再决定是否需要更复杂的映射。
容易答错的地方
- 以为分支不执行就不会报语法错误
- 静态
import放在代码块里是语法错误,解析整个模块时就会被发现。if (false)不会帮它避开检查。若需要条件判断,把判断留在运行时,改用import()。 - 以为动态导入直接返回导出的函数
import('./charts.js')返回 Promise;等待完成后得到模块命名空间对象。要调用命名导出的renderReport,先从该对象上取成员。省掉await就直接访问成员,拿到的是 Promise,不是模块导出。
面试官还会怎么问?
静态 import 必须写在文件最前面吗?
不必。它必须在模块顶层,但可以出现在其他顶层语句之后。加载器仍先建立依赖,再执行模块代码,所以书写顺序不会把静态导入变成运行时操作。
在普通脚本里能写静态 import 吗?
不能。浏览器的普通 <script> 不是模块,直接写导入声明会报语法错误。要使用静态模块语法,脚本须按模块加载;动态 import() 则可在支持它的脚本环境中使用。
动态 import() 和 require() 一样吗?
两者都能把加载写进执行路径,但机制不同。import() 遵循 JavaScript 模块加载机制,返回 Promise;CommonJS 的 require() 通常同步返回导出值。把两者当成同一个 API,会误判结果的类型和加载时序。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。