前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 循环依赖中函数声明能否安全调用
JavaScript模块与语言机制

循环依赖里,对方导出的是函数声明,我能在顶层直接调用吗?

能调用。函数声明在模块实例化阶段就已经创建好了,比模块里任何代码执行都早,所以对方还没开始执行,这边也能调它。但只限 function 声明,const fn = () => {} 在执行到那一行之前读不到,会抛 ReferenceError。

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

循环依赖里能不能提前用

  1. function 声明实例化阶段就创建好,任何时候都能调用
  2. const 箭头函数执行到声明那行才赋值,之前读取抛 ReferenceError
  3. 函数体内的变量函数能调,但它读的对方模块变量可能还在暂时性死区

函数声明能调用只解决了一半问题。函数里如果读了同模块的 const 配置,而那个模块还没执行到那一行,照样报错。

核心回答

先记住这个答案

能安全调用。ESM 在执行任何模块代码之前,会先给每个模块创建好作用域,这一步叫实例化,函数声明就在这时候创建并赋值。所以循环依赖里,即使导出方一行代码都还没跑,导入方调用它导出的函数声明也没问题。换成 const fn = () => {} 就不行了,const 要等代码执行到那一行才赋值,提前访问会抛 ReferenceError。

  • function 声明在实例化时就创建,循环里能提前调用
  • 箭头函数、函数表达式、class 提前用会抛 ReferenceError
  • 函数能调用,不代表函数体里用到的变量已经初始化
  • export default function 声明也会提升

声明和箭头函数的对比

a.js 是入口,它导入 b.js;b.js 又反过来导入 a.js,并且在顶层直接调用 a.js 导出的两个函数。

一个能调,一个报错JavaScript
// a.js(入口)
import './b.js'
export function hello() { return 'hello' } // 函数声明
export const hi = () => 'hi' // 箭头函数赋给 const

// b.js
import { hello, hi } from './a.js'
console.log(hello()) // 'hello',a.js 还没执行,但函数已经存在
console.log(hi()) // ReferenceError: Cannot access 'hi' before initialization

执行顺序是:a.js 的第一行 import 让 b.js 先执行。这时 a.js 里一行代码都还没跑。hello 是函数声明,实例化时已经放进 a.js 的作用域里了,所以能调。hi 是 const,要等 a.js 执行到那一行才赋值,现在还在暂时性死区里。

为什么函数声明这么特殊

这和普通脚本里的函数提升是同一件事,只是发生在模块层面。模块实例化时,引擎扫描一遍顶层声明:function 声明直接创建函数对象并赋值;let、const、class 只登记名字,不赋值;var 登记名字并设成 undefined。

ESM 的导入是实时绑定,b.js 里的 hello 就是 a.js 里那个 hello 变量本身。实例化结束时它已经有值,所以不管 a.js 执行到哪儿,b.js 都能调用它。

export default function hello() {} 也是函数声明,同样会被提升,匿名的 export default function () {} 也一样。但 export default () => {}、export default someFactory() 这类表达式,要等执行到那一行才有值,提前调用会抛 ReferenceError。

函数能调,不代表一定安全

函数声明能被调用,但函数体执行时读到的其他变量不一定准备好了。

函数体里读了未初始化的常量
如果 a.js 写成 const PREFIX = '>' 和 export function hello() { return PREFIX + 'hello' },那么 b.js 顶层调用 hello() 时,PREFIX 还没执行到赋值,函数内部读它照样抛 ReferenceError。报错位置在 a.js 里,排查时容易以为是 a.js 自己写错了。 所以循环依赖里提前调用的函数,最好是不依赖模块顶层状态的纯函数。依赖状态的逻辑,放到入口执行完之后再调用。
回答前,多想一步

容易答错的地方

以为导出的函数都能提前调用
只有 function 声明可以。项目里很多人习惯写 export const fn = () => {},这种写法在循环依赖里提前调用一定会报错。改成函数声明是最快的修法之一。
以为是 ESM 导出做了特殊处理
不是导出机制特殊,是函数声明本身在实例化时就创建。同样的函数声明不导出,在自己模块里也能在声明之前调用。ESM 做的只是让导入方看到的就是这个变量本身。
以为 CommonJS 里也一样
CommonJS 里 b.js 拿到的是 a.js 当时的 exports 对象。exports.hello = hello 这行还没执行,exports.hello 就是 undefined,调用会报 TypeError: a.hello is not a function,即使 hello 是函数声明也没用。
试着用自己的话回答

面试官还会怎么问?

class 导出在循环依赖里能提前 new 吗?

不能。class 声明和 let 一样有暂时性死区,执行到声明那行之前访问会抛 ReferenceError。要在循环里提前用,只能改结构,把类抽到不参与循环的模块里。

如果入口换成 b.js,上面的例子还会报错吗?

不会。入口是 b.js 时,它先执行依赖 a.js,a.js 执行完 hi 已经有值,再回到 b.js 调用两个函数都正常。循环依赖的代码能不能跑,和谁先被加载有关,这也是它难排查的原因。

打包成 CommonJS 格式后还能这样用吗?

要看打包工具怎么转换。Babel 转 CommonJS 时会把函数声明的导出赋值提到模块最前面,所以通常还能提前调用。但这依赖工具的实现细节,不要把业务正确性建立在这上面。

从一道题,走向一组知识

把知识连起来

模块与语言机制

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

同样是循环依赖里提前使用对方导出,换成 let 就会碰上暂时性死区。

模块与语言机制

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

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

参考资料

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

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

本题目录
  1. 先记住这个答案
  2. 声明和箭头函数的对比
  3. 为什么函数声明这么特殊
  4. 函数能调,不代表一定安全
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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