先记住这个答案
会提升。ES6 模块的加载分三个步骤:解析、实例化、执行。在解析阶段,import 被静态分析;实例化阶段创建所有模块绑定,然后才执行模块体。因此 import 声明的绑定在模块代码执行前已建立,import 语句的位置不影响绑定是否可用。但这不同于 var 提升:import 绑定是只读实时视图,没有自身初始化过程。在没有循环依赖的常规加载中,依赖模块会先于导入模块完成执行,所以绑定已完成初始化(若导出的变量是 let x; 未赋值,则为 undefined);若存在循环依赖,导出模块可能尚未执行到初始化语句,此时访问绑定会因暂时性死区而抛出 ReferenceError。
- import 绑定在模块求值前已创建
- 绑定只读实时,值随导出模块变化
- 变量提升概念不适用于 import 的值
机制:链接先于执行
ESM 加载不像传统脚本即时执行,而是经历三阶段。解析阶段只分析语法,构建模块记录。随后实例化阶段根据 import 引用递归加载依赖模块,为每个模块创建环境记录,并将导入绑定连接到对应导出绑定。最后才执行模块顶层代码。由于 import 的绑定在实例化时已经建立,运行时无论在代码哪里写 import,绑定都已存在,表现出“提升”。
这种提升不同于变量初始化:var 声明会被初始化为 undefined,而 import 绑定只是对导出绑定的实时引用,本身没有默认值。真正影响值可用性的是模块的执行顺序:在没有循环依赖的常规加载中,导出模块会先于导入模块执行完毕,因此导出绑定已完成初始化(未赋值的 let 变量会是 undefined)。但若存在循环依赖,某模块可能在导出绑定初始化之前就被访问,此时会触发暂时性死区(TDZ),抛出 ReferenceError。也就是说,提升只保证绑定存在,不保证值可用——值是否可用取决于导出模块的执行进度,而不是 import 的位置。
具体场景:工具模块与调用顺序
考虑一个简单的配置模块 config.js,它导出常量 export const apiUrl = '...'。在入口 app.js 中,我们在文件第一行使用 console.log(apiUrl),而 import 语句写在最后一行。尽管通常认为 import 应在使用前,由于实例化先执行,输出不会因位置出错。
若把 config.js 改为导出 export let apiUrl; setTimeout(() => { apiUrl = '...' }, 0);,由于 config.js 的顶层代码同步执行完时 apiUrl 仅被声明但尚未被 setTimeout 回调赋值,app.js 顶层立即访问会得到 undefined,不会触发 TDZ。这显示绑定已存在但值尚未就绪。若要获得最终值,需要等待异步回调完成后再访问。若存在循环依赖且导出模块尚未执行到 let apiUrl 的初始化语句,则访问会触发 TDZ。
边界:模块解析失败与 TDZ
提升有一个硬边界:模块必须能成功解析。如果 import 目标路径无效或语法错误,整个模块图不会进入执行阶段,因此任何代码都不会运行。所以不能用条件或动态路径写 import,它是静态的。此外 import 只能出现在顶层,不能嵌套在块或函数中。
若在循环依赖中,实例化顺序可能导致某个绑定在初始化阶段不可用,此时访问会触发 ReferenceError,因为导出模块尚未初始化某些 let 绑定。但这不在题目范围。处理办法是避免在模块顶层读取依赖值,而是通过函数调用等延迟到依赖完成。
容易答错的地方
- 把 import 提升与 var 提升混淆
- 一些人认为 import 也会像 var 一样被初始化为 undefined,允许在模块执行早期读取并得到 undefined。实际上 import 绑定是导出绑定的别名,没有自身初始化过程。依赖模块中的代码决定了值何时可读。若导出的是
let x;(未赋值),读取得到undefined;只有在导出模块尚未执行到该声明(如循环依赖)时,读取才会抛出 ReferenceError。 - 认为 import 可放在块内
- 有开发者误以为 import 声明可以像局部变量一样放在 if 或 try 块中。但 ES 模块是静态结构,必须在模块顶层。即使 import 写在块内,解析阶段就会失败。正确做法是用动态 import 来有条件导入。
面试官还会怎么问?
如果 import 语句写在模块底部,前面就使用导入值,总是安全吗?
在无循环依赖的常规模块加载中,导出模块先于导入模块完成执行,绑定总有值(若为 let 未赋值则为 undefined),不会出现 TDZ。若存在循环依赖,导出方可能尚未初始化对应绑定,此时访问会抛出 ReferenceError(暂时性死区)。为代码清晰起见,仍建议将 import 写在文件顶部,但这是风格建议而非必要性。
import * as ns 与 import { name } 在提升机制上有区别吗?
没有本质区别,两者在实例化阶段都会建立绑定,绑定到 namespace 对象或具体导出。区别在于实时绑定访问方式:具名导入直接绑定到导出变量,namespace 则通过属性反映。
动态 import() 是否也有提升?
没有。动态 import() 返回 Promise,在调用时才开始加载依赖,不属于模块的静态实例化阶段。因此它可以出现在函数或条件等任意位置。动态导入不会预创建绑定。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。