前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库ESM import 声明提升与执行顺序
JavaScript模块与语言机制

ESM 的 import 声明会被提升吗?对代码执行顺序有什么影响?

会被提升。ESM 的 import 声明在模块代码执行之前就已经全部处理完了,不管 import 写在文件的哪个位置,导入的绑定在模块的第一行就可以使用。这和 require() 完全不同——require() 是执行到那一行才去加载模块。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#模块与语言机制
先看核心答案读代码示例
理解线索

模块加载的三个阶段

  1. 解析引擎扫描所有 import 声明,确定依赖关系
  2. 加载与链接递归加载所有依赖模块,建立模块之间的绑定
  3. 执行从叶子模块开始,深度优先地执行每个模块的代码

循环依赖时,被循环引用的模块可能还没执行完,let 和 const 声明的绑定处于暂时性死区(TDZ),读取会报 ReferenceError。

核心回答

先记住这个答案

ESM 的 import 声明会被提升到模块的最顶部。引擎在执行模块代码之前,会先扫描所有 import 声明,递归加载并执行所有依赖模块。等所有依赖都准备好了,当前模块的代码才开始从上往下执行。所以不管 import 写在文件的第 1 行还是第 100 行,导入的绑定在模块一开始就可以使用。

  • import 声明会被提升,在模块代码执行前完成
  • 不管 import 写在哪,导入的绑定在第一行就可用
  • 被导入的模块先于当前模块执行,深度优先
  • require() 不提升,在执行到那一行时才加载

先看一个直观的例子

把 import 写在 console.log 后面,按直觉应该先打印再导入,但实际上 import 已经被提升了。

import 写在后面也能用JavaScript
// main.mjs
console.log(add(1, 2)) // 3 —— import 已经提升,add 可用

import { add } from './math.mjs'

// math.mjs
// export function add(a, b) { return a + b }

虽然 import 写在 console.log 后面,引擎不会按文本顺序来处理 import。它先把所有 import 收集起来,加载完依赖模块之后,add 就已经绑定好了。等到执行 console.log(add(1, 2)) 时,add 已经是一个可用的函数了。 实际项目里不推荐把 import 写在文件中间,虽然语法上没问题,但会降低可读性。ESLint 的 import/first 规则会强制 import 写在文件顶部。

模块的执行顺序:深度优先

当 A 依赖 B,B 依赖 C 时,执行顺序是 C 先执行,然后 B,最后 A。引擎按深度优先的方式遍历模块图,叶子模块最先执行。

三个模块的执行顺序JavaScript
// a.mjs
import { b } from './b.mjs'
console.log('A 执行')

// b.mjs
import { c } from './c.mjs'
console.log('B 执行')
export const b = 'b'

// c.mjs
console.log('C 执行')
export const c = 'c'

// 加载 a.mjs 后控制台输出:
// C 执行
// B 执行
// A 执行

这和 require() 的行为有个关键区别。require() 是同步执行到那一行时才去加载和执行目标模块,代码的文本顺序就是执行顺序。ESM 是在所有代码执行之前就完成了整个模块图的加载和链接。

和 require() 的行为对比

CommonJS 的 require() 不会被提升。引擎执行到 require('./b') 那一行时才去加载 b.js、执行它的代码、返回 module.exports。如果 require 写在 if 里,条件不满足就不会加载。

ESM 的 import 不能写在 if 里——它是声明,不是表达式,只能出现在模块的顶层。想要条件加载就得用 import()(动态导入)。

这个区别也影响了循环依赖的处理。require() 遇到循环依赖时,返回的是目标模块当前已经执行完的部分导出(可能是空对象或者不完整的对象)。ESM 遇到循环依赖时,绑定本身在解析阶段就建立了,但如果源模块还没执行到初始化语句,let 和 const 的绑定会处于暂时性死区(TDZ),读取会报 ReferenceError。

回答前,多想一步

容易答错的地方

以为 import 提升和 var 提升是同一种机制
var 提升是把变量声明移到函数顶部,但赋值留在原地,所以提升后值是 undefined。import 提升不一样——它不只是"声明了一个名字",而是在执行前就完成了模块加载和绑定的建立。提升之后绑定的值就是源模块导出的实际值(或者实时绑定引用),不会是 undefined。
以为可以在 import 前面修改导入的值
有人以为 import 写在后面,前面的代码就能"抢先"使用同名的变量。其实不行。import 声明创建的绑定和 let 以及 const 一样,在同一作用域里不允许有同名的变量声明。试图在 import 之前用 let add = ... 声明同名变量会报 SyntaxError。
试着用自己的话回答

面试官还会怎么问?

如果一个模块被多个文件 import,它会执行几次?

只执行一次。引擎维护一个模块映射表,每个 URL 对应的模块只会被加载和执行一次。后续的 import 直接从映射表里拿到已经创建好的绑定,不会重新执行模块代码。

循环依赖时 import 提升会导致什么问题?

如果 A 导入 B,B 又导入 A,加载顺序取决于谁先被入口引用。假设先加载 A,执行到 A 的 import B 时去加载 B,B 里 import A 发现 A 还在加载中,就使用 A 已经建立但还没初始化的绑定。如果 A 的导出是 export const x = 1,在 B 执行时 x 还处于 TDZ,读取会报 ReferenceError。用 export function 导出函数就没有这个问题,因为函数声明会被提升。

import 提升对副作用有什么影响?

如果你 import './polyfill.mjs' 只是为了执行它的副作用(比如往全局对象上挂东西),不管这行 import 写在文件哪里,polyfill 都会在当前模块的任何代码之前执行。这通常是你想要的效果,但也意味着你没办法控制"先执行一些代码再加载 polyfill"。需要延后加载副作用模块的话,只能用 import()。

从一道题,走向一组知识

把知识连起来

模块与语言机制

只写 import './polyfill.js' 不接收任何导出,有什么用?会执行吗?

import 提升还意味着同一模块被多个 import 引用时只执行一次,副作用导入 import './polyfill' 也是如此。

模块与语言机制

import { a as b } 重命名以后,还能读到原模块的最新值吗?

import { a as b } 重命名后的 b 同样在模块顶部就可用,提升行为不受重命名影响。

放回知识体系中复习JavaScript 进阶知识

参考资料

  • JavaScript modules

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 先看一个直观的例子
  3. 模块的执行顺序:深度优先
  4. 和 require() 的行为对比
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑