想象你经营一家大型餐厅。早期的后厨没有独立工位,所有食材、调料、刀具都堆在同一张大桌子上。一百个厨师伸手就能拿到任何东西——表面上是方便,可一旦有人动了你的盐罐、切了你的葱、拿走了你的刀,整个后厨就乱成一锅粥。更麻烦的是,谁也不知道谁用了什么,出了错也查不到源头。 你决定改革:把后厨分成一个个独立的工位——备菜台、炒锅区、甜点房、酒水吧。每个工位有自己的小仓库,明确写着“我需要什么、我提供什么”。其他工位需要时,就按规定的通道去取。这套分工逻辑,就是 JavaScript 的模块系统。 从把所有代码塞进一堆 script 标签,到 CommonJS、AMD、UMD,再到最终被语言标准化的 ESM,JavaScript 模块化走过了一条从“手工管理”到“标准化自动化”的路。读懂这段故事,你就同时理解了 require 和 import 为什么长得不一样、为什么浏览器和 Node 加载模块的方式不同,以及你的项目该选哪种模块方案。
前端工程化绕不开模块选型。理清 CommonJS 到 ESM 的演进脉络,配置 Webpack/Vite、发布 npm 包、排查依赖报错时,你就不再靠猜。
目录
可视化演示
展示CommonJS同步加载机制、ESM静态导入分析及模块加载五步流程,交互切换理解JavaScript模块化演进核心原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+modeData.cjs.caption+'
模块化演进:从大锅饭到独立工位
早期的 JavaScript 没有一个正式的模块系统。页面里要引入多个 JS 文件,就在 HTML 里依次写上多个 script 标签,所有变量和函数被倒入同一个全局作用域。就像餐厅后厨把所有食材堆在一张大桌子上,看似伸手就能拿到,实则谁都能改谁的,出了错根本无从追查。
当页面越来越复杂,脚本越来越多,“大锅饭”的混乱就爆发了:后加载的脚本可能覆盖先加载的同名变量,某个文件依赖的库没有被提前引入就直接报错。开发者只能靠“手动调整 script 标签顺序”来维持脆弱的平衡,这就是依赖管理混乱的根源。于是,模块化的呼声出现了——每个文件都应该有自己的作用域,只暴露必要的接口,明确声明需要的依赖。
模块化的核心诉求可以浓缩为一个简单的公式:把每个脚本的“领地”隔离出来。
正是因为认清了大锅饭的问题,社区开始探索模块化方案,才有了后面 CommonJS、AMD、UMD、ESM 这一路演进。记住这个起点:模块化要解决的,就是把“一张大桌子”改造成“一个个带独立仓库的工位”。
- 全局命名空间浏览器中的 window、Node 中的 global,是所有脚本共享的顶层作用域
- ∑ 求和符号把每个 script 标签的影响叠加到同一个空间里
- 变量与函数每个 JS 文件里顶层声明的所有东西,都会成为全局对象的一个属性
- 叠加的后果后加载的同名变量会覆盖先加载的,依赖顺序完全靠手工保证
CommonJS规范:后厨现场取料
CommonJS 像后厨传统的“传菜口取料”模式:厨师做到一半发现需要面粉,就放下锅铲走到仓库门口喊一声“我要面粉”,管仓人当面把面粉递给他,他拿到手才回去继续炒菜。整个过程当场发生、一步等一步、拿到才能继续。这就是 CommonJS 的哲学——运行时同步加载。
CommonJS 诞生于 Node.js 环境。服务端读本地文件非常快,同步等待造成的阻塞几乎可以忽略,所以这种“现场取料”的模式完全够用。它的核心是两个 API:require() 是加载函数,参数是模块路径;module.exports 是导出对象,模块通过它公开自己的接口。开发者也可以把 exports 当作 module.exports 的别名来添加属性。
CommonJS 的调用链路可以表达为:
注意,CommonJS 的 require 是函数调用,所以它可以在 if 语句里、循环里、甚至任意运行时位置动态拼接路径。这就是“动态加载”——执行到哪一行,才加载那一个模块。就像厨师做到一半才发现没葱,临时跑一趟仓库。
CommonJS 让 Node.js 后厨第一次有了“独立工位”的秩序:每个文件有自己的作用域,通过 require 和 module.exports 明确往来。但它有一个明显的局限:它是为 Node 设计的同步方案,浏览器端无法直接使用——浏览器下载文件是异步的,不能像读本地磁盘一样瞬间返回。
- require(模块id)CommonJS 的加载入口,参数是相对路径或包名
- 同步执行Node 挂起当前任务,读取并执行目标模块的全部代码
- module.exports被加载模块把对外接口挂到这个对象上
- 返回调用方require 执行结束后,把 exports 对象交给等在前面的代码
AMD规范:异步点餐提前下单
浏览器环境完全不一样。页面通过 HTTP 加载 JS 文件,网络延迟不可控,如果像 CommonJS 那样同步等待,页面会卡死。想象前厅点餐:客人坐下,服务员先记录下所有需要的菜(声明依赖),然后一次性下单,厨房做好一道端一道——客人不需要自己跑去厨房干等。这就是 AMD 的思路:异步模块定义。
AMD(Asynchronous Module Definition)的核心函数是 define。开发者声明一个模块时,把它的名字、依赖数组和工厂回调一起交给 define。依赖模块全部异步加载完成后,工厂回调才被调用,回调的返回值就是模块对外开放的接口。RequireJS 是 AMD 最流行的实现。这种“先点单、后上菜”的模式很好地解决了浏览器的异步问题。
AMD 的定义结构可以写成:
AMD 的“前置依赖声明”是一大进步:模块的依赖关系在定义时就能看到,不再需要手动调整 script 标签顺序。但它也有代价——写起来繁琐,define 嵌套层级一多,代码可读性直线下降。它像前厅点餐:等待时间体验好了,但点餐流程比“直接去厨房拿”要复杂得多。
AMD 证明了“异步加载”在浏览器端的价值,但它的方案没有被语言采纳,更像一个过渡性的“点餐系统”。真正终结这场混乱的,是语言层面的标准答案——ESM。
- defineAMD 的核心函数,用来定义模块
- id给模块起一个唯一标识名,方便其他模块引用
- 依赖数组提前声明该模块需要的所有其他模块,浏览器据此并行下载
- factory依赖全部就绪后才执行的回调,返回值即对外 API
UMD兼容方案:万能适配插头
AMD 和 CommonJS 分庭抗礼时,开发者发布一个库面临一个尴尬的问题:写 CommonJS 风格,浏览器用不了;写 AMD 风格,Node 用不了;写全局变量风格,又显得不够现代。于是 UMD(Universal Module Definition)出现了。它像一个万能适配插头——你出差到一个陌生国家,不确定酒店插座是美标、欧标还是英标,万能插头全都能插。
UMD 不是一种独立的新规范,而是把 CommonJS、AMD 和全局变量三种方案“缝”进同一个文件。它用一个立即执行函数包裹库代码,在函数开头做环境检测:如果 module 对象存在,就走 CommonJS 的 module.exports;如果 define.amd 存在,就走 AMD 的 define;如果两者都不在,就把库挂到 window 上作为全局变量。
UMD 的逻辑可以表示为一个分支结构:
UMD 的确做到了“一个文件走天下”,但它只是个权宜之计:代码里充满了环境判断的样板,既不优雅,也无法被静态分析优化。万能插头虽然百搭,但真正先进的酒店早就统一了插座标准——那个标准就是 ESM。UMD 作为历史过渡产物,如今主要还出现在一些老牌 npm 包的 dist 文件里。
- UMD =一个环境分支判断,本质是模块导出的多路分发
- module.exports(Node 环境)检测到 CommonJS 的 module 对象时,走 Node 导出
- define(AMD 环境)检测到
define.amd标记时,走 AMD 导出 - 全局变量(无模块系统)两者都不存在时,把库挂到 window 上兜底
ESM标准:中央厨房的菜单制
ESM(ES Module)像现代中央厨房的管理模式:餐厅开门营业前,主厨已经把一周的菜单、每道菜的食材清单、供应商、配送时间全部定好了。开工前食材已经按份配好,谁也不用临时跑到仓库去拿。这种“提前规划”就是 ESM 的精髓——编译期静态分析。
ESM 是 ECMAScript 2015(ES6)正式加入语言规范的原生模块系统,由 TC39 标准委员会制定。它用 import 和 export 两个声明式关键字替代了 require 函数和 module.exports 对象。import 和 export 必须写在文件顶层,不能嵌套在条件语句里——故意的,因为正是这种“死板”让引擎在代码运行前就能完整地分析出依赖关系。
ESM 的静态特性可以表示为:
ESM 不仅支持命名导出 export { a, b },还支持默认导出 export default,以及动态导入 import()(返回 Promise,可用于按需加载)。现代浏览器通过 <script type="module"> 原生支持 ESM,Node.js 14+ 也通过 "type": "module" 配置完整支持。
中央厨房的菜单制让一切变得可规划、可优化。开工前食材清单就打印好了,每个工位按清单领料,不需要做到一半才跑仓库。这就是 ESM“静态”两个字的真正价值——它不只是规范了语法,更让整个工具链(打包器、编译器、浏览器)得以围绕模块结构进行深度优化。
- import { 所需导出 }声明式导入,用花括号列出需要使用的具名导出
- from '模块路径'指定模块的来源路径或包名
- 编译期静态分析代码尚未执行,引擎就能解析出所有 import 关系
- 依赖关系图所有 import 连接成的树状结构,供引擎并行加载和打包器优化
加载机制对比:动态取料 vs 静态配餐
CommonJS 和 ESM 最核心的差异,可以浓缩成一句话:开火后找菜 vs 开工前备菜。CommonJS 的 require 是一个函数调用,代码执行到那一行才去加载模块,加载期间阻塞后续代码,而且路径可以是运行时拼接的变量——这是“动态取料”。ESM 的 import 是声明式语句,在代码执行前就被静态分析,引擎和打包器可以预先知道整个依赖图,浏览器甚至可以并行下载多个模块——这是“静态配餐”。
这两种机制决定了它们的能力边界。因为 CommonJS 是动态的,工具无法在不运行代码的情况下知道一个模块到底导出了什么,所以没法做 tree-shaking(摇树优化)。而 ESM 是静态的,export 和 import 的名字在编译期就一目了然,打包器可以安全地删除那些没有被任何 import 引用的导出,从而减小最终的代码体积。
这两者的差异可以概括为:
除了 tree-shaking,加载同步性的差异也很关键:CommonJS 同步阻塞,但 Node 读本地文件快,影响不大;ESM 支持异步协议,浏览器并行下载多个模块文件时优势明显。另外,CommonJS 的 require 可以写在函数里实现条件加载,ESM 的静态 import 不能——但有 import() 动态导入函数来弥补这个缺口。
理解“动态取料 vs 静态配餐”,你就掌握了两套模块哲学的本质。选型时不是看哪个“更好”,而是看你的运行环境和优化需求。最后走到工程实践,答案就清楚了。
- 静态依赖图 (ESM)ESM 在编译期即可构建完整的模块依赖关系
- ⇒ 推导出静态结构带来的直接能力
- tree-shakingWebpack、Rollup 等打包器的“摇树”优化
- 剔除未引用导出没有被任何 import 使用的 export 会被安全移除
工程实践:新老厨房的过渡与混用
现实中的餐厅不可能一夜之间改成中央厨房——老后厨还在用传菜口,新员工已经习惯菜单制。JavaScript 世界也处在这个过渡期:新的 ESM 生态在蓬勃生长,海量的老 npm 包仍是 CommonJS。于是“双厨房”并行成了常态:有的档口用老办法,有的档口用新办法,中间靠几个“传菜员”(打包器与兼容层)衔接。
工程实践的原则可以归结为三句话。第一,新项目优先用 ESM:Node.js 18+ 上在 package.json 里设置 "type": "module" 即可,浏览器端直接写 <script type="module">;第二,老项目不必强制迁移:CommonJS 依然被完全支持,.cjs 扩展名可以显式声明一个文件是 CommonJS;第三,用打包器统一入口:Vite、Webpack、Rollup 会自动把源码里的 ESM 编译成目标环境能跑的格式,开发者不需要手动切换。
项目选型可以用一个决策表表示:
跨体系互引也有成熟方案:CommonJS 代码可以通过动态 import() 加载 ESM 模块,这是 Node 官方推荐的过渡方式;ESM 加载 CommonJS 模块时,Node 会把 module.exports 包装成默认导出。打包器则会自动处理大部分互操作细节,你在配置 exports 字段时同时提供 "import" 和 "require" 两个入口。
新老厨房的过渡期会持续很多年,但方向已经明确:ESM 是终局。就像中央厨房的菜单制最终会取代传菜口的临时取料——不是因为它最“潮”,而是因为它是语言标准、可静态分析、能优化打包体积,整个前端工具链都在向它靠拢。
- 项目配置根据运行环境和维护成本决定模块系统
- "type""module":Node.js 项目开启 ESM 解析的关键字段
- require + exports老项目保持 CommonJS 语法,不做破坏性迁移
- script type=module浏览器原生加载 ESM 的方式
AI 应用场景
场景一:前端工程化构建配置
阶段:AI 应用前端交付与集成
解决的问题:AI 应用的前端界面往往需要加载可视化图表库、状态管理库、HTTP 请求库等多个第三方包。传统方式用多个 script 标签手动引入,版本冲突和加载顺序问题频发;切到 ESM + 打包器后,依赖关系由编译期静态分析自动处理,还能通过 tree-shaking 大幅减小最终包体。
具体案例:用 Vite 搭建一个 AI 对话仪表盘,源码全部采用 ESM 编写,package.json 配置 "type": "module";引入 ECharts 时使用具名导入只打包用到的图表类型;遇到老旧的 CommonJS 库,Vite 的 optimizeDeps 会自动预构建成 ESM 格式,开发时开箱即用。
场景二:Node AI 服务与 SDK 发布
阶段:模型服务部署与封装
解决的问题:后端 AI 服务需要用 Node.js 暴露推理 API,既要支持内部工具用 require 调用,又要给浏览器端页面用 import 加载。如果模块系统选错,会出现 require is not defined 或 Unexpected token 'export' 这类运行时错误。
具体案例:开发一个基于 Transformers.js 的轻量推理服务,服务端代码用 ESM 编写,Node 18 直接运行;对外发布推理 SDK 时,用打包工具同时产出 ESM 和 CJS 两个入口,package.json 的 exports 字段用条件导出区分 "import" 和 "require",让任何消费方都能正确加载。
Python 演示
import time class Module: """单个模块:名称、依赖列表、工厂函数、导出对象""" def __init__(self, name, deps, factory): self.name = name self.deps = deps self.factory = factory self.exports = {} def run_commonjs(modules, entry): """模拟 CommonJS:执行到 require 才同步加载""" cache = {} def require(name, trace): if name in cache: return cache[name] mod = modules[name] for dep in mod.deps: require(dep, trace + " -> " + name) mod.factory(mod.exports) cache[name] = mod.exports print("[require] 同步加载:", trace, "->", name) return mod.exports t0 = time.time() require(entry, "entry") print("CommonJS 总耗时: %.4fs" % (time.time() - t0)) def run_esm(modules, entry): """模拟 ESM:编译期先静态分析整棵依赖树,再执行""" def analyze(name, seen): mod = modules[name] for dep in mod.deps: analyze(dep, seen + [name]) return mod print("--- 编译期静态分析 ---") analyze(entry, []) print("依赖树已构建:", list(modules.keys())) executed = {} def execute(name): if name in executed: return executed[name] mod = modules[name] for dep in mod.deps: execute(dep) mod.factory(mod.exports) executed[name] = mod.exports print("[import] 执行:", name) t0 = time.time() execute(entry) print("ESM 总耗时: %.4fs" % (time.time() - t0)) def build_modules(): """组装依赖链: main -> utils -> tools, main -> math""" tools = Module("tools", [], lambda e: e.update({"rate": 10})) utils = Module("utils", ["tools"], lambda e: e.update({"add": lambda a, b: a + b + e["rate"]})) math = Module("math", [], lambda e: e.update({"sqr": lambda x: x * x})) main = Module("main", ["utils", "math"], lambda e: e.update({"calc": lambda x: e["sqr"](e["add"](x, 1))})) return {m.name: m for m in [tools, utils, math, main]} if __name__ == "__main__": print("===== CommonJS 同步加载 =====") run_commonjs(build_modules(), "main") print("\n===== ESM 编译期静态分析 =====") run_esm(build_modules(), "main")
def require(name, trace):模拟 CommonJS 的 require 函数,依赖深度优先同步加载mod.factory(mod.exports)执行模块工厂函数,填充导出对象,模拟 module.exportsdef run_esm(modules, entry):模拟 ESM 加载流程:先 analyze 后 executeanalyze(entry, [])编译期完成整棵依赖树的静态分析,此时不执行任何模块代码executed = {}与 CommonJS 的 cache 类似,保证每个模块只执行一次
常见误区
误区我听说 ESM 支持异步加载,那它是不是比 CommonJS 慢?毕竟 require 是同步的,看起来更“直接”。
正解加载速度不能简单用“同步 vs 异步”判断。CommonJS 同步加载意味着一次只加载一个、加载完才能执行下一步;ESM 的静态依赖图让浏览器可以并行下载多个模块文件,整体加载效率往往更高。import 语句本身是声明性的,它不负责“下载”这个动作,真正的加载由引擎调度。现代的 HTTP/2 多路复用之下,并行下载多个小文件比串行下载更快。<br>如果把 CommonJS 比作“每次只能跑一趟仓库拿一种食材”,ESM 就是“开工前一次性下单,供应商并行送货上门”,哪个更快,一目了然。
为何常见:看到“异步”两个字就联想到“慢”,忽略了异步的核心优势是“并发”而非“延迟”。
误区Node.js 就是为 CommonJS 而生的,所以我用 Node 写代码只能用 require,不能写 import。
正解Node.js 从 12 版本开始实验性支持 ESM,14 版本稳定支持,18 版本已经可以无障碍地直接用 import/export。你只需要把文件扩展名改为 .mjs,或者在 package.json 中设置 "type": "module"。Node 官方已经明确把 ESM 作为未来的主推方向,CommonJS 则作为兼容方案继续保留。<br>这就像老后厨虽然还在用传菜口,但新的中央厨房已经正式营业,新员工完全可以按新流程工作。
为何常见:大量早期教程和 Stack Overflow 老答案都是基于 Node 6~10 的 CommonJS 时代,信息更新滞后。
误区UMD 和 ESM 都是 JavaScript 的模块系统,我项目里选一个用就行,反正都是“模块标准”。
正解UMD 不是一种与新规范平级的标准,它是把 CommonJS、AMD、全局变量三种方案“缝”进一个文件的兼容层。ESM 才是语言级标准,由 TC39 制定,得到了浏览器和 Node 的原生支持。两者定位完全不同:UMD 解决的是“一个库在不同环境都能跑”的过渡问题,ESM 解决的是“JavaScript 语言层面到底该怎么组织模块”的根本问题。<br>把 UMD 和 ESM 相提并论,就像把“万能转换插头”和“国际统一插座标准”当成同类物品——前者是应急工具,后者是终极方案。
为何常见:看到 UMD 和 ESM 都是三个字母的缩写,且都出现在模块相关语境中,容易误以为它们是同一层次的概念。
误区`exports` 和 `module.exports` 不是同一个东西吗?那我写 `exports = { add: ... }` 也能正常导出吧?
正解初始时 exports 确实是指向 module.exports 的引用,但直接给 exports 重新赋值会让它指向一个全新的对象,彻底切断与 module.exports 的联系,导出自然失效。正确做法有两种:给 exports 添加属性(exports.add = ...),或者直接修改 module.exports(module.exports = { add: ... })。<br>就像你把快递单上的收件人改成别人,快递员照单投递,就再也不会送到你手上了——引用指向变了,对象还是那个对象吗?
为何常见:只记住了“exports 是 module.exports 的简写”,忽略了引用赋值的底层机制。
误区浏览器只能通过 script 标签加载 JS 文件,import/export 是 Node 和打包工具才支持的东西。
正解现代浏览器(Chrome 61+、Firefox 60+、Safari 11+)早已原生支持 ESM,直接在 HTML 里写 <script type="module"> 就能使用 import/export,不需要任何打包工具。不仅如此,浏览器原生 ESM 还支持模块的并行下载和按需执行。打包器(Webpack/Vite)之所以仍然存在,是为了处理 JSX、TypeScript、资源文件等更广泛的需求,而不是因为浏览器不能用 ESM。<br>这就好比你家的烤箱早就支持新式烤法了,但专业面包房仍然用商用级设备——不是因为家用不支持,而是因为商用设备能干更多活。
为何常见:E SM 规范是 2015 年推出的,很多人对浏览器支持的记忆停留在 2015 年前后,多年未更新认知。
面试题
Q1. CommonJS 中 require('./math') 的加载行为是?
- A.同步阻塞加载← 正确答案
- B.异步并行加载
- C.编译期静态解析
- D.预加载后回调执行
答案:A。CommonJS 的 require 是函数调用,执行到那一行才去读取并执行目标模块,加载期间阻塞后续代码。B 是 AMD 或 ESM 并行加载的特征,C 是 ESM 的静态分析,D 是 AMD 工厂回调的模式。就像厨师现场去仓库取料,拿到手才继续炒菜。
Q2. 以下哪个是 ESM 的导入语法?
- A.const fs = require('fs')
- B.import fs from 'fs'← 正确答案
- C.include fs from 'fs'
- D.using fs = require('fs')
答案:B。import fs from 'fs' 是 ESM 的默认导入语法。A 是 CommonJS 的 require,C 和 D 是编造的干扰项。记住特征:ESM 用 import 关键字,CommonJS 用 require 函数,这是两者的直观分水岭。
Q3. AMD 规范诞生的核心原因是?
- A.解决浏览器同步阻塞← 正确答案
- B.替代Node运行时
- C.统一全局变量命名
- D.支持CSS样式加载
答案:A。AMD 的诞生背景是浏览器环境——网络加载 JS 无法像 Node 读本地文件那样同步等待,AMD 用“前置声明依赖 + 异步加载”解决了阻塞问题。B 说反了,AMD 是给浏览器用的;C 是模块化的动机之一但非 AMD 特有;D 与 JS 模块系统无关,CSS 有自己的加载机制。
Q4. 关于 CommonJS 与 ESM 差异描述正确的有?
- A.CommonJS 动态加载同步执行← 正确答案
- B.ESM 依赖关系在编译期确定← 正确答案
- C.CommonJS 原生支持 tree-shaking
- D.ESM 浏览器端已原生可用← 正确答案
答案:ABD。CommonJS 是运行时动态加载、同步执行,A 正确;ESM 的 import/export 在编译期就能解析出完整依赖图,B 正确;tree-shaking 依赖静态结构,CommonJS 无法实现,C 错误;现代浏览器通过 <script type="module"> 原生支持 ESM,D 正确。C 选项是常见的混淆点——tree-shaking 是 ESM 的专属福利。
Q5. UMD 模块的本质是?
- A.一种标准化的语言规范
- B.三种环境的兼容包装← 正确答案
- C.Node专属的加载API
- D.浏览器打包工具插件
答案:B。UMD(Universal Module Definition)不是语言规范,而是把 CommonJS、AMD、全局变量三种导出方式缝进同一个文件的兼容包装,运行时通过环境检测自动选择。它像万能适配插头,让一个库在 Node、RequireJS、直接挂 window 三种场景下都能工作。A 错在它不是独立规范,C/D 更是完全偏题。
Q6. 以下哪些是 ESM 的合法 export 写法?
- A.export const version = 1← 正确答案
- B.export function greet() {}← 正确答案
- C.module.exports = { greet }
- D.export default { version }← 正确答案
答案:ABD。A 是具名导出变量,B 是具名导出函数,D 是默认导出对象,三者都是 ESM 的合法语法。C 是 CommonJS 的写法,混入 ESM 会报语法错误。可以这样记:ESM 使用 export 关键字直接修饰声明,CommonJS 则依赖 module.exports 这个特殊对象。
Q7. Node.js 项目切换为 ESM 的推荐配置是?
- A.安装 Babel 并改写全部语法
- B.设置 type 字段为 module← 正确答案
- C.修改 Node 进程环境变量
- D.使用 webpack 替代 Node 运行
答案:B。在 package.json 中设置 "type": "module",Node 就会以 ESM 模式解析 .js 文件,这是官方推荐做法。A 中的 Babel 是转译工具但非必须;C 没有这种环境变量;D 是打包器,不能替代 Node 运行时。设置好 type 字段后,就可以直接使用 import/export 语法。
Q8. tree-shaking 得以实现的前提条件是?
- A.require 的可动态拼接路径
- B.import 的静态可分析结构← 正确答案
- C.define 的前置依赖数组
- D.UMD 的运行时环境分支
答案:B。tree-shaking 依赖模块的导出和引用关系在编译期就完全确定,ESM 的 import/export 是顶层静态声明,满足这个条件。CommonJS 的 require 路径可以动态拼接,工具无法静态分析,所以做不了 tree-shaking;AMD 的前置依赖数组只声明了模块级依赖,无法精细到导出名;UMD 是运行时分支,与静态分析无关。
Q9. 关于动态 import() 的说法正确的有?
- A.返回 Promise 对象← 正确答案
- B.支持代码分割懒加载← 正确答案
- C.只能在 Node 端使用
- D.可以在条件语句中调用← 正确答案
答案:ABD。动态 import() 返回 Promise,可以在运行时按需加载,非常适合路由懒加载和代码分割,A/B 正确。它可以在 if 语句、循环等任意位置调用,D 正确。C 错误——动态 import() 在浏览器和 Node 中都被支持。它既打破了静态 import 不能在条件中使用的限制,又保持了模块化的按需加载能力。
Q10. CommonJS 模块被 ESM import 引入时,Node 内部做了什么?
- A.直接执行 CJS 代码
- B.将 CJS 包装为 ESM 默认导出← 正确答案
- C.强制转换为 AMD 模块
- D.报错并终止运行
答案:B。Node 加载 CommonJS 模块时,会将其 module.exports 整体包装成 ESM 的默认导出,因此你可以用 import pkg from 'cjs-module' 来引入。如果 CJS 模块在 module.exports 上挂的是具名属性,Node 还会尝试做静态分析,尽量提供具名导出的支持,但底层机制始终是“默认导出 + 属性探测”。A 是浏览器全局脚本的行为,C/D 不符合 Node 的实际设计。