真实面经题目 · 原创解析
了解模块化么?说说 CommonJS 和 ESModule
这题不是只考 CommonJS 和 ESModule 的语法名称,而是看你能不能从加载流程解释绑定语义、循环依赖、Tree Shaking 和工程互操作。
真实面经题目 · 原创解析
这题不是只考 CommonJS 和 ESModule 的语法名称,而是看你能不能从加载流程解释绑定语义、循环依赖、Tree Shaking 和工程互操作。
CommonJS 用 require 和 module.exports,模块在运行时同步执行,第一次 require 会执行文件并缓存 module.exports,后续 require 复用缓存;ESModule 用 import 和 export,是语言标准,依赖关系在执行前就能静态分析,先建立导入导出绑定再执行模块。语义上,CJS 拿到的是 module.exports 当时暴露出来的值或对象引用,解构后通常不会自动更新;ESM 的 import 是只读 live binding,导出方变量变了,导入方读到的是最新绑定。工程里还要注意 Tree Shaking 依赖 ESM 静态结构和副作用判断,循环依赖时 CJS 可能拿到未初始化完成的 exports,ESM 可能遇到 TDZ,Node 和构建工具混用时要看 default 导出、package.json type、.cjs/.mjs 和转换语义。
CommonJS 的导出入口是 module.exports,exports 只是初始指向 module.exports 的快捷引用;exports.foo = foo 能生效,exports = foo 只是改了本地变量。ESModule 的 import/export 是语法层能力,import 必须在顶层静态声明,导入名本身不能被重新赋值。
CJS 是运行时同步加载:require 时解析路径,命中缓存就返回缓存的 module.exports;未命中就创建 module 对象,把文件包装成函数执行,执行结束后缓存结果。ESM 先解析静态 import/export,构建模块依赖图,实例化导入导出绑定,再按依赖顺序求值。
CJS 不能简单说全是值拷贝。require 返回 module.exports 的当前值;如果是对象,消费者持有同一个对象引用;如果消费者解构出属性,拿到的是那一刻的局部值;如果导出方后来整体替换 module.exports,旧消费者不会自动更新。ESM 的 import 是导出方绑定的只读视图,导出方变量变化后,导入方再次读取会看到新值。
ESM 的 import/export 结构在执行前可见,打包器可以建立依赖图,判断某个导出是否被使用,再结合 sideEffects 标记和副作用分析删除未使用代码。但 Tree Shaking 不是用了 ESM 就一定生效,顶层副作用、动态访问、转译方式和包配置都会影响结果。
CJS 会先把正在初始化的 module 放进缓存,循环另一端 require 回来时可能拿到半成品 exports。ESM 在求值前已经创建绑定,循环里如果读取尚未初始化的 let、const 或 class 绑定,可能触发 TDZ;如果延迟到函数调用后再读,通常可以工作。
Node 项目会受 package.json type、.cjs、.mjs、exports 条件导出影响。ESM 引 CJS 时默认导入通常对应整个 module.exports,具名导入是否稳定依赖运行时或打包器推断;CJS 引 ESM 常用动态 import。Babel、TypeScript、Webpack、Rollup、Vite 还会引入 default 包装和 __esModule 兼容层。
exports 初始只是 module.exports 的引用。exports.foo = foo 是在 module.exports 对象上加属性;exports = fn 只是让局部变量 exports 指向新函数。require 返回的是 module.exports,所以整体替换必须写 module.exports = fn。
require 返回 module.exports 的当前值。如果它是对象,消费者持有对象引用;如果解构出属性,拿到的是那一刻的局部值;如果导出方后面整体替换 module.exports,旧消费者不会自动跳到新对象。
import { count } 不是 const { count } = obj。它是导入方对导出方 count 绑定的只读视图,导出方 count++ 后,导入方再次读取会看到新值,但导入方不能给 count 重新赋值。
CJS 可能拿到半初始化的 exports,字段是 undefined 或旧对象上的部分字段;ESM 先创建绑定再求值,若读取尚未初始化的 let/const/class 绑定会触发 TDZ。
ESM 的静态 import/export 让打包器在执行前知道依赖图和导出使用情况,这是 Tree Shaking 的前提;但顶层副作用、sideEffects 配置和转译产物会影响能否安全删除。
ESM 引 CJS 时默认导入通常拿到整个 module.exports,具名导入依赖运行时或打包器推断;CJS 引 ESM 往往要动态 import。排查时还要看 __esModule、default 包装和 esModuleInterop。