这条线真正的分水岭不是语法,而是依赖何时被确定:IIFE/UMD/CJS 都是运行时解析,只有 ESM 是编译期静态确定——tree-shaking、循环依赖处理、顶层 await 全都源于这一点。

顺着帖子把 UMD 补完:它其实是运行时嗅探,先看有没有 define,再看有没有 module.exports,都没有才挂 window,所以一个文件能同时喂给 RequireJS、Node 和 <script>。代价是它必须把所有分支都写进去,浏览器里仍然拿不到静态依赖图。

ESM 的关键差别在三点:import 必须写在顶层(不能条件式)、导出是绑定而非拷贝(CJS 那个"值快照"的坑在这里不存在,改 count 引入方实时可见)、依赖图在解析阶段就建好。配合 package.json 里 "type": "module" 或 .mjs 后缀即可启用。

新项目直接上 ESM 没问题,但互操作要留意:ESM 里 import 一个 CJS 包,module.exports 会变成默认导出的命名空间,具名导入靠 Node 的静态分析猜,猜不到就得 import pkg from 'x'; const { a } = pkg;。

延伸两个坑:ESM 里没有 __dirname/__filename/require,要用 import.meta.url 配 fileURLToPath 换算;循环依赖上 CJS 返回未完成的 exports 对象、ESM 则可能抛 TDZ 报错,表现不一样,别指望行为一致。