ES Module 和 CommonJS 有什么区别?循环依赖时会发生什么?

ES Module 和 CommonJS 有什么区别?循环依赖时会发生什么?

ES Module 通过 import/export 表达模块依赖,导入通常与导出的绑定相连。CommonJS 则通过 require 执行模块并取得 module.exports,可以在运行过程中按条件加载。

因此,ESM 的静态结构更方便分析依赖与未使用代码,但不能简单说 ESM 永远异步、CommonJS 永远不能加载 ESM。Node.js 的互操作规则会随版本变化,现代版本可以同步 require 满足条件、没有顶层 await 的 ESM。

循环依赖时,两边也不是同一种处理方式。CommonJS 可能取得尚未初始化完的导出对象;ESM 则要遵守绑定的初始化时机,过早访问可能报错。

项目迁移时,除了改语法,还要检查 package.json 的 type、文件扩展名、依赖的导出方式、路径和加载时机。

为什么改完 import,项目仍然跑不起来?

先把模块格式和编译结果分开

假设咱们把一个旧 Node.js 服务迁移到 ESM。源代码看起来用了 import,但构建工具也可能把它转成 require。反过来,文件扩展名和 package.json 又会影响 Node 如何解释输出文件。

因此,先检查真正交给运行时的文件。不要只看 TypeScript 源码,就认定程序正在按 ESM 执行。

在 Node.js 中,.mjs、.cjs 以及 package.json 的 type 等配置共同影响格式识别。浏览器中的原生模块又有自己的加载规则。

静态依赖与运行时加载,带来哪些区别?

ESM 的普通 import 声明提供可分析的依赖关系。运行代码前,模块系统可以先建立关联,之后按规则初始化和执行模块。

CommonJS 的 require 是运行时调用,因此可以放在条件分支里。模块第一次执行后通常进入缓存,后续加载可以复用相应导出。

ESM 也支持动态 import()。动态 import() 是按需加载的表达式,返回 Promise;静态 import 则不能放进 if。

这也是为什么 Tree Shaking 要看模块结构和副作用。支持静态分析是有利条件,仍然不等于打包工具可以随便删除任何看起来没用的代码。

"导出的值是副本"为什么说得不够准确?

ESM 导入的变量通常是导出绑定的只读视图,导出模块更新绑定,使用方可以观察到更新;使用方不能任意给这个导入绑定重新赋值。

CommonJS 返回的是 module.exports 当时对应的值。如果它是对象,使用方拿到的是对象引用,对象属性修改也可以被观察到。因此,笼统说 CommonJS 只会复制值,同样不对。

区别更明显的情况,是使用方先解构取得一个普通值,而原模块后来更新导出对象的属性;解构出来的局部变量不会自动变成持续连接的绑定。

分析时写出具体的导出、赋值和读取动作,比背"一个动态、一个静态"更可靠。

循环依赖要检查第一次读取发生在何时

假设 a 依赖 b,b 又依赖 a。如果 b 在 a 初始化完成之前读取 a 的数据,就可能遇到问题。

CommonJS 可以返回部分导出,使用方不能假设所有字段已经写好。ESM 可以建立相互引用的绑定,但尚未初始化的绑定被过早读取,仍可能触发错误。

把读取推迟到函数调用时,可能改变结果,但不能因此认为所有循环都安全。更好的处理通常是把共享定义拆成独立模块,或者明确初始化入口。

验证迁移时,准备最小项目分别检查模块缓存、循环引用和互操作。Node 的 require(ESM) 还要检查是否存在顶层 await;有些加载路径必须改成动态 import。

本题机制参考,Node.js ESM 官方文档、MDN JavaScript 模块。

面试官继续追问

ESM 没有 require,是不是不能使用 CommonJS 包?

可以按运行时的互操作规则使用,但默认导出、命名导出识别和导出对象要逐包确认。不能把 CommonJS 对象里的任何字段都认定为可靠命名导出。

用了 ESM,包一定更小吗?

不会自动保证。副作用、重新导出、依赖格式和打包配置都会影响删除能力。需要检查构建产物,而不是看源文件扩展名。

为什么本文不放 Python 逐行对应代码?

Python 的导入缓存、模块对象和循环导入规则不同。它不能验证 Node 的格式识别或 ESM 绑定行为,这类迁移应在对应的 JS 运行时里做最小实验。

面试速记卡

  • 先看产物,源文件语法不等于运行时模块格式。
  • ESM,依赖关联与导出绑定有明确规则。
  • CommonJS,require 执行并取得 module.exports。
  • 循环依赖,重点检查初始化前的读取。
  • 互操作,按 Node 版本与顶层 await 条件判断。

我是程序员Sunday。本文收录于 sunday面试指南,原文与配图可继续阅读。

相关推荐
Fuyo_111943 分钟前
C++ 运行时多态与虚函数机制全面剖析【面试重点】
开发语言·c++·面试
孟健1 小时前
Claude Code 同需求写两版对比:装不装 frontend-design 差多远?
前端·ai编程·claude
IT规划师1 小时前
一文讲透五种发布机制:滚动、蓝绿、金丝雀、红黑与 AB
后端·面试
OpsEye2 小时前
大模型 Token 计量不准,是什么原因,怎么解决??
javascript·ai编程
梦de天空一米16883 小时前
【一个重复id让游戏卡在62%,聊聊配置表检查】
前端
ryn483983 小时前
01 一个功能模块,怎么分层才不乱
前端·ai编程·claude
Amos_Web3 小时前
通过 Rspack 源码学 Rust(五):读懂 Result 与问号运算符的错误返回
前端
杨航 AI3 小时前
JEV的用法
java·前端·数据库
Amos_Web3 小时前
Rspack 源码解析(三十五):rspack_error 如何用 miette 渲染带源码定位的错误链
前端
去伪存真3 小时前
高敏资产场景下的大模型协同设计架构
前端·agent