前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 循环依赖 let 暂时性死区
JavaScript模块与语言机制

循环依赖里,一个模块在顶层读另一方导出的 let 变量,会发生什么?

会抛 ReferenceError: Cannot access 'x' before initialization。导入名指向的就是对方的 let 变量本身,对方还没执行到声明那一行,这个变量处在暂时性死区,谁读都报错。换成 var 不报错但读到 undefined,换成函数声明则可以正常调用。

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

对方还没执行时,读三种导出的结果

  1. let 和 const处在暂时性死区,读取抛 ReferenceError
  2. var实例化时已设成 undefined,不报错但值不对
  3. 函数声明实例化时已创建好,可以正常调用

报错的前提是在模块顶层同步读取。放进函数、事件回调或 setTimeout 里晚点读,对方已经执行完,就能读到正常的值。

核心回答

先记住这个答案

会抛 ReferenceError: Cannot access 'x' before initialization。ESM 的导入是实时绑定,导入名指向的就是导出方的那个 let 变量。循环依赖里,如果导出方还没执行到 let x = ... 这一行,这个变量就处在暂时性死区,也就是「已经声明但还不能访问」的状态,导入方在顶层读它自然报错。等导出方执行完再读就正常。

  • 对方的 let 还没执行到声明,导入方读取抛 ReferenceError
  • 暂时性死区跟着实时绑定延伸到了导入方
  • var 读到 undefined,函数声明可以正常调用
  • 修法:把读取挪进函数,或拆掉循环

复现这个报错

main.js 是入口,导入了 store.js;store.js 为了拿配置又导入了 main.js。

顶层读取对方的 letJavaScript
// main.js(入口)
import { store } from './store.js'
export let theme = 'dark'

// store.js
import { theme } from './main.js'
console.log(theme) // ReferenceError: Cannot access 'theme' before initialization
export const store = { theme }

执行顺序是:main.js 依赖 store.js,所以 store.js 先执行。这时 main.js 一行代码都没跑,theme 已经被登记,但还没执行到 let theme = 'dark'。store.js 在顶层读它,就撞上了暂时性死区。 这个报错会中断 store.js 的执行,进而整个模块图加载失败,main.js 也不会执行。浏览器里的表现是控制台一条红色报错,页面没有内容。

为什么导入方也受暂时性死区影响

暂时性死区本来是单个作用域里的概念:let 声明之前的那段代码不能访问这个变量。放在模块里,同一个规则延伸到了导入方,因为导入方拿到的不是值的拷贝,而是指向那个变量的引用。变量还没初始化,从哪个模块去读都一样。

对比一下其他声明方式。var 在实例化阶段就被设成 undefined,所以提前读不报错,但读到的是 undefined,store 里的 theme 会一直是 undefined,比报错更难发现。函数声明在实例化阶段就创建好了,提前调用没问题。

两种修法

第一种是延迟读取。不在顶层读,而是放进函数里,调用时对方已经执行完了。比如把 export const store = { theme } 改成 export function getStore() { return { theme } },入口执行完再调用 getStore(),读到的就是 'dark'。

第二种是拆掉循环。store.js 真正需要的只是 theme 这个配置,它不该从入口文件拿。把 theme 挪到单独的 config.js,main.js 和 store.js 都从 config.js 导入,循环就没了。这是更推荐的做法,循环依赖本身就说明模块边界没划清。

回答前,多想一步

容易答错的地方

说会读到 undefined
那是 CommonJS 或者 var 导出的表现。ESM 里 let、const、class 导出都不会给你 undefined,而是直接抛错。
为了不报错把 let 改成 var
报错是没了,但读到的是 undefined,业务逻辑拿着错误的值继续跑,问题被藏了起来。正确做法是调整读取时机或者拆掉循环。
以为是 import 语句位置的问题
有人会把 import 挪到文件底部试图修复。import 声明不管写在哪儿都会先处理,挪位置对执行顺序没有任何影响。
试着用自己的话回答

面试官还会怎么问?

如果入口换成 store.js,还会报错吗?

不会。这时 main.js 先执行,theme 已经赋值,store.js 再执行就能读到 'dark'。循环依赖的代码能不能跑,取决于哪个模块先被加载,这正是它难排查的地方。

项目里怎么提前发现循环依赖?

可以用 madge --circular src 扫描依赖图,列出所有循环。Webpack 项目可以用 circular-dependency-plugin 在构建时报警。ESLint 插件 eslint-plugin-import 的 import/no-cycle 规则也能在写代码时提示。

导出方后来修改了 theme,导入方能看到吗?

能。只要读取发生在初始化之后,导出方执行 theme = 'light',导入方下一次读到的就是 'light'。但导入方自己不能给 theme 赋值,会抛 TypeError。

从一道题,走向一组知识

把知识连起来

模块与语言机制

为什么 script type=module 加载跨域脚本必须过 CORS,普通 script 却不用?

和本题关系不大,讲的是浏览器加载模块文件时的跨域要求。

模块与语言机制

一个模块里导出两个同名的变量会怎样?和默认导出能重名吗?

同样是 ESM 导出的报错,那题是解析阶段的 SyntaxError,本题是执行阶段的 ReferenceError。

参考资料

  • JavaScript:模块与语言机制参考资料

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

本题目录
  1. 先记住这个答案
  2. 复现这个报错
  3. 为什么导入方也受暂时性死区影响
  4. 两种修法
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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