尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

面试题:CommonJS 和 ESM 有什么区别?

面试题:CommonJS 和 ESM 有什么区别? 这道题不要答得太散。真正适合面试的主线其实是CJS 和 ESM 的核心区别抓住两点1、标准来源不同CJS是社区标准ESM是语言标准。2、依赖关系的确认时机不同CJS是运行时ESM是运行时编译时。由此进一步产生语法、加载方式、静态分析能力、运行环境等差异。一、面试题CommonJS 和 ECMAScript ModulesESM有什么区别1. 核心思路一句话CommonJS 是以运行时加载为核心的模块系统而 ECMAScript ModulesESM是语言标准定义的、以静态模块结构为核心的模块系统因此两者在语法、依赖分析、加载机制和工程化能力上存在差异。2. 结构化逻辑思维可以按照这条主线回答CommonJS vs ECMAScript ModulesESM │ ┌────────┴────────┐ ↓ ↓ 标准来源 依赖确定时机 │ │ ↓ ↓ CommonJS社区方案 CommonJS运行时确定 ESMECMAScript标准 ESM静态分析模块结构 │ ┌────────┼────────┐ ↓ ↓ ↓ 语法 加载 工程化 │ │ │ require 同步 Tree Shaking module import 静态分析 exports import() 更容易优化所以面试时不要一上来背十几个区别。先说“标准来源 依赖确定时机”其他区别都是由这两个核心点展开的。3. CommonJS 和 ESM 的标准来源有什么区别核心思路CommonJS 是社区提出的模块规范Node.js 对其进行了广泛采用ESM 则是 ECMAScript 官方语言标准的一部分。CommonJSCommonJS 并不是 ECMAScript 官方标准而是早期 JavaScript 社区提出的一套模块化规范。Node.js 早期采用并发展了 CommonJS因此我们在 Node.js 中经常看到constfsrequire(fs);module.exports{foo:bar};它主要通过require()module.exports exports这些运行时机制实现模块化。ESMESM 是 ECMAScript 官方标准中的模块系统。它通过语言层面的模块语法实现importfoofrom./foo.js;exportdefaultfoo;或者exportconstnameTom;exportfunctionsayHello(){}因此CommonJS ↓ 社区模块方案 ↓ 主要通过运行时 API 实现 ESM ↓ ECMAScript 官方语言标准 ↓ 通过语言级 import / export 语法实现4. CommonJS 和 ESM 在语法上有什么区别核心思路CommonJS 使用运行时函数和对象完成模块导入导出ESM 使用语言级的 import/export 语法。CommonJS// math.jsfunctionadd(a,b){returnab;}module.exports{add};导入const{add}require(./math.js);console.log(add(1,2));ESM// math.jsexportfunctionadd(a,b){returnab;}导入import{add}from./math.js;console.log(add(1,2));核心区别CommonJS ↓ require() module.exports exports ESM ↓ import export5. 为什么 CommonJS 的 require() 是运行时加载而 ESM 的 import 是静态模块结构这是这道题最重要的原理。核心思路CommonJS 的 require() 本质上是运行时执行的函数调用因此依赖关系可以根据运行时条件决定ESM 的静态 import 属于模块语法JavaScript 引擎可以在执行模块代码之前分析模块依赖。CommonJS运行时确定依赖例如letmoduleName;if(process.env.NODE_ENVdevelopment){moduleName./dev.js;}else{moduleName./prod.js;}constconfigrequire(moduleName);这里require()的参数可以来自变量。因为代码开始执行 ↓ 判断条件 ↓ 得到 moduleName ↓ 执行 require() ↓ 确定真正需要加载的模块所以 CommonJS 的依赖关系可以在运行过程中确定。甚至可以if(condition){constfoorequire(./foo.js);}ESM静态 importESM 的静态导入import{foo}from./foo.js;模块路径必须符合静态模块语法要求不能这样constmoduleName./foo.js;import{foo}from moduleName;// ❌ 非法也不能这样if(condition){import{foo}from./foo.js;// ❌ 非法}也不能这样for(;;){import{foo}from./foo.js;// ❌ 非法}原因是解析模块 ↓ 发现 import ↓ 建立模块依赖关系 ↓ 加载/链接模块 ↓ 执行模块代码也就是说静态 import 不需要等业务代码真正运行后才能知道模块之间是什么关系。6. ESM 不能动态导入模块吗能。这是非常容易被答错的地方。ESM 有两种导入方式ESM ├── 静态 import │ ↓ │ 编译/链接阶段即可分析 │ └── 动态 import() ↓ 运行时异步加载例如constmoduleName./foo.js;constmoduleawaitimport(moduleName);console.log(module);这里的import()是动态 import属于运行时加载并且返回 Promise。所以不能简单说“ESM 是同步的”或者“ESM 是异步的”。更准确的是ESM 的静态 import 是静态模块依赖声明动态 import() 则是在运行时异步加载模块。7. CommonJS 和 ESM 的加载机制有什么区别CommonJS在 Node.js 中CommonJS 模块通常表现为require() ↓ Node.js 模块加载器 ↓ 定位模块 ↓ 读取文件 ↓ 编译模块 ↓ 执行模块 ↓ module.exports ↓ 返回结果并且 CommonJS 模块具有缓存机制。例如constarequire(./foo);constbrequire(./foo);console.log(ab);通常会得到true因为同一个模块通常只执行一次之后从模块缓存中获取结果。8. 为什么 CommonJS 中可以使用 arguments、module、exports、__filename这是理解 CommonJS 底层原理非常重要的一点。Node.js 并不是简单地把 CommonJS 文件原封不动地直接执行。可以近似理解为 Node.js 会把模块代码包装成类似(function(exports,require,module,__filename,__dirname){// 你的 CommonJS 模块代码});例如你的代码console.log(__filename);module.exports{name:Tom};可以近似理解成(function(exports,require,module,__filename,__dirname){console.log(__filename);module.exports{name:Tom};});所以console.log(arguments.length);在 CommonJS 模块中能够存在并不奇怪。因为模块代码本身处在一个函数包装环境中。9. CommonJS 和 ESM 的顶层 this 有什么区别在 Node.js CommonJS 模块中console.log(this);顶层this通常指向module.exports而不是全局对象。例如console.log(thismodule.exports);通常为true而 ESM 中console.log(this);结果是undefined因此在 Node.js 中可以记成CommonJS 顶层 this ↓ module.exports ESM 顶层 this ↓ undefined另外ESM 并不只能运行在浏览器中。现代 Node.js 同样原生支持 ESM。10. CommonJS 和 ESM 为什么对 Tree Shaking 的支持能力不同核心思路Tree Shaking 的关键是“运行前静态分析哪些代码真正被使用”而 ESM 的静态模块结构更容易被构建工具分析CommonJS 的动态 require() 会增加静态分析难度。例如 ESM// utils.jsexportfunctionadd(a,b){returnab;}exportfunctionsubtract(a,b){returna-b;}如果import{add}from./utils.js;console.log(add(1,2));构建工具可以比较明确地知道utils.js ├── add ← 使用 └── subtract ← 未使用因此可以尝试删除subtract()相关代码。这就是 Tree Shaking 的基础之一。CommonJS 的问题例如constmoduleNamegetModuleName();constutilsrequire(moduleName);在构建阶段getModuleName() 到底返回什么 ↓ 运行之前可能不知道 ↓ 到底加载哪个模块 ↓ 静态分析困难因此ESM 天生更适合静态分析和 Tree Shaking。但这里也要注意一个重要边界不能简单说“CommonJS 完全不能 Tree Shaking。”现代构建工具可以对部分 CommonJS 代码进行转换和分析例如通过 CommonJS 转换插件把部分 CommonJS 转换成 ESM。因此准确表达应该是原生 CommonJS 的动态特性天然不利于静态分析因此 Tree Shaking 对 ESM 的支持更直接、更可靠CommonJS 通常需要额外转换或受到代码形式限制。11. 为什么 ESM 的静态 import 有利于工程化优化可以从整个流程理解ESM ↓ 静态 import / export ↓ 构建工具无需执行完整业务代码 ↓ 分析模块依赖关系 ↓ 构建依赖图 ↓ 判断未使用的导出 ↓ Tree Shaking ↓ 删除无用代码 ↓ 减少最终产物体积因此ESM ↓ 静态模块结构 ↓ 更容易分析 ↓ 更容易优化这也是现代前端工程普遍偏向 ESM 的重要原因之一。12. CommonJS 和 ESM 的循环依赖有什么区别核心思路两者都可以出现循环依赖但处理机制不同ESM 的模块链接机制能够处理循环依赖并通过实时绑定让模块之间形成依赖关系而 CommonJS 更容易出现“拿到尚未完成初始化的导出值”等问题。例如A ↓ B ↓ A形成A → B → ACommonJSCommonJS 执行模块时如果发现模块正在加载可以从缓存中拿到当前模块的部分导出结果。因此循环依赖场景下可能拿到{}或者拿到一个尚未完成初始化的导出对象。例如// a.jsconstbrequire(./b);module.exports.nameA;console.log(b.name);// b.jsconstarequire(./a);module.exports.nameB;console.log(a.name);这里就可能出现A 加载 ↓ A require B ↓ B 加载 ↓ B require A ↓ A 尚未执行完 ↓ B 得到 A 当前已经导出的内容因此 CommonJS 循环依赖要特别注意模块初始化顺序。ESMESM 在模块执行之前会先建立模块之间的依赖关系。大致解析模块 ↓ 建立模块依赖图 ↓ 模块链接 ↓ 确定导入导出关系 ↓ 按照规范执行模块ESM 的导入通常不是简单地把一个值复制过来而是建立与导出绑定的关系。因此它能够更系统地处理循环依赖。但是ESM 并不是“有循环依赖就一定没问题”。如果循环依赖导致模块在变量初始化之前访问它仍然可能出现ReferenceError所以面试中不要说“ESM 可以完美解决循环依赖。”正确说法是ESM 有更明确的模块链接和绑定机制可以规范化处理循环依赖但如果在模块初始化完成前访问相关绑定仍然可能报错。13. CommonJS 和 ESM 的主要区别总结对比维度CommonJSECMAScript ModulesESM标准来源社区规范ECMAScript 官方标准典型环境Node.js浏览器、Node.js 等导入require()import导出module.exports/exportsexport静态模块结构不属于其核心设计是核心设计依赖确定运行时静态 import 可提前分析动态加载require(variable)等import()动态 import本身就是运行时调用import()返回 Promise顶层 thisNode.js通常为module.exportsundefinedTree Shaking天然不利于静态分析更适合静态分析循环依赖可能拿到未完成初始化的导出有模块链接机制但初始化顺序仍重要模块包装Node.js CommonJS 有函数包装ESM 不采用 CommonJS 那种函数包装14. 使用场景CommonJS 适合① Node.js 旧项目例如constexpressrequire(express);大量传统 Node.js 项目仍然存在 CommonJS 代码。② 需要运行时决定加载模块例如constadapterrequire(./adapters/${type}.js);但实际工程中如果需要动态加载现代 ESM 通常更推荐constadapterawaitimport(./adapters/${type}.js);ESM 适合① 现代前端项目importReactfromreact;import{createRoot}fromreact-dom/client;② 需要 Tree Shakingimport{debounce}from./utils.js;③ 需要明确模块依赖关系入口模块 ↓ 模块 A ↓ 模块 B ↓ 模块 C构建工具可以根据静态 import/export 构建完整依赖图。④ Node.js 新项目Node.js 现在也支持 ESM因此不能再简单地认为CommonJS Node.js ESM 浏览器正确理解应该是Node.js ├── CommonJS └── ESM 浏览器 └── ESM15. 边界场景CommonJS 和 ESM 能不能混用可以但需要注意互操作规则。例如现代 Node.js 项目中可能同时存在package.json ↓ 项目中 ├── a.cjs ├── b.mjs └── package.json常见约定.cjs → CommonJS .mjs → ESM或者通过package.json的{type:module}影响.js文件的模块解释方式。因此面试不要说“一个项目只能选择 CommonJS 或 ESM。”实际上两者可以通过 Node.js 的模块互操作机制进行组合但具体互相导入存在规则和限制。16. 一个完整示例CommonJSmath.js// CommonJS 使用 module.exports 导出模块成员。functionadd(a,b){returnab;}functionsubtract(a,b){returna-b;}// 将需要暴露给其他模块的内容挂载到 module.exports。module.exports{add,subtract};index.js// require() 是 CommonJS 的运行时模块加载方式。const{add,subtract}require(./math.js);console.log(add(10,5));// 15console.log(subtract(10,5));// 5ESMmath.js// ESM 使用 export 导出模块成员。exportfunctionadd(a,b){returnab;}exportfunctionsubtract(a,b){returna-b;}index.js// import 是 ESM 的静态模块导入语法。// 构建工具和 JavaScript 引擎可以提前分析该模块依赖。import{add,subtract}from./math.js;console.log(add(10,5));// 15console.log(subtract(10,5));// 5ESM 动态导入如果需要根据运行时条件决定加载哪个模块asyncfunctionloadModule(type){// type 的值只有运行时才能确定。constmodulePathtypedev?./dev.js:./prod.js;// import() 是动态导入。// 它在运行时加载模块并返回 Promise。constmoduleawaitimport(modulePath);returnmodule;}loadModule(dev).then(module{console.log(module);});这正好体现静态 import ↓ 依赖关系可以提前分析 动态 import() ↓ 运行时决定加载哪个模块17. 需要纠正的几个常见错误错误一CommonJS 只能在 Node.js不准确。更准确CommonJS 最典型的运行环境是 Node.js但其他 JavaScript 运行时和构建工具也可以实现或兼容 CommonJS。错误二ESM 只能在浏览器错误。现代 Node.js 同样支持 ESM。浏览器 → ESM Node.js → ESM CommonJS错误三require() 是同步机制所以 import 是异步机制这个说法过于简单。应该说CommonJS 的 require() 在 Node.js 中通常是同步加载调用ESM 的静态 import 是模块语法其核心特征是静态依赖分析而不是简单的“异步导入”。ESM 的动态 import() 才明确是异步加载并返回 Promise。错误四CommonJS 的顶层 this 是全局对象错误。Node.js CommonJS 模块中thismodule.exports通常为true。ESM 顶层thisundefined错误五ESM 可以解决所有循环依赖问题错误。正确ESM 有规范化的模块链接和绑定机制可以处理循环依赖但如果循环依赖导致在初始化完成之前访问绑定仍然可能报错。错误六Tree Shaking 只能支持 ESMCommonJS 永远不能说得太绝对。正确ESM 的静态结构天然适合 Tree ShakingCommonJS 的动态特性不利于静态分析但构建工具可以对部分 CommonJS 代码进行转换和分析。18. 这道题的主要矛盾和次要矛盾主要矛盾① 标准来源CommonJS → 社区模块规范 ESM → ECMAScript 官方标准② 依赖关系什么时候确定CommonJS → 运行时确定 ESM 静态 import → 可以在执行前分析模块依赖关系这是最核心的区别。次要矛盾由上述区别进一步产生静态分析能力 ↓ Tree Shaking ↓ 工程化优化 运行时加载 ↓ 动态 require ↓ 更灵活 ESM 模块链接 ↓ 循环依赖处理机制不同 模块系统不同 ↓ 顶层 this、模块包装、互操作规则不同所以面试时不要把所有区别平铺罗列。先抓主要矛盾再解释次要矛盾。19. 最推荐的面试回答结构如果面试官只问“CommonJS 和 ESM 有什么区别”建议按照第一层一句话概括 ↓ 第二层标准来源 ↓ 第三层依赖确定时机 ↓ 第四层由此产生的工程化差异 ↓ 第五层补充动态 import 和使用场景而不是一上来讲this 循环依赖 Node.js 浏览器 Tree Shaking 缓存 函数包装 ...这样容易把一道简单题答复杂。20. 满分答案CommonJS 和 ESM 最核心的区别有两个标准来源不同以及模块依赖关系的确定时机不同。第一标准来源不同。CommonJS 是社区提出的模块规范Node.js 对它进行了广泛采用主要通过require()、module.exports和exports实现ESM 则是 ECMAScript 官方标准通过import和export实现。第二依赖确定时机不同。CommonJS 的require()是运行时调用所以可以根据运行时条件决定加载哪个模块ESM 的静态import属于语言级模块语法JavaScript 引擎和构建工具可以在执行模块代码之前分析依赖关系。这个区别直接带来一个重要结果ESM 更适合静态分析因此更容易实现 Tree Shaking 等工程化优化CommonJS 的动态加载能力更强但天然不利于静态分析。另外ESM 也支持运行时动态加载可以使用import()它返回 Promise。Node.js 现在同时支持 CommonJS 和 ESM所以不能简单理解成“CommonJS 是 Node.jsESM 是浏览器”。一句话总结CommonJS 更偏运行时模块加载ESM 更偏静态模块结构真正的核心区别是模块依赖什么时候能够被确定。
返回列表