☰
面试题:CommonJS 和 ESM 有什么区别?
2026/9/28 21:00:06 网站建设 项目流程

这道题不要答得太散。真正适合面试的主线其实是:

CJS 和 ESM 的核心区别,抓住两点:
1、标准来源不同,CJS是社区标准,ESM是语言标准。
2、依赖关系的确认时机不同,CJS是运行时,ESM是运行时+编译时。
由此进一步产生语法、加载方式、静态分析能力、运行环境等差异。

一、面试题:CommonJS 和 ECMAScript Modules(ESM)有什么区别?

1. 核心思路(一句话)

CommonJS 是以运行时加载为核心的模块系统,而 ECMAScript Modules(ESM)是语言标准定义的、以静态模块结构为核心的模块系统,因此两者在语法、依赖分析、加载机制和工程化能力上存在差异。


2. 结构化逻辑思维

可以按照这条主线回答:

CommonJS vs ECMAScript Modules(ESM) │ ┌────────┴────────┐ ↓ ↓ 标准来源 依赖确定时机 │ │ ↓ ↓ CommonJS:社区方案 CommonJS:运行时确定 ESM:ECMAScript标准 ESM:静态分析模块结构 │ ┌────────┼────────┐ ↓ ↓ ↓ 语法 加载 工程化 │ │ │ require 同步 Tree Shaking module import 静态分析 exports import() 更容易优化

所以面试时不要一上来背十几个区别。

先说“标准来源 + 依赖确定时机”,其他区别都是由这两个核心点展开的。


3. CommonJS 和 ESM 的标准来源有什么区别?

核心思路

CommonJS 是社区提出的模块规范,Node.js 对其进行了广泛采用;ESM 则是 ECMAScript 官方语言标准的一部分。

CommonJS

CommonJS 并不是 ECMAScript 官方标准,而是早期 JavaScript 社区提出的一套模块化规范。Node.js 早期采用并发展了 CommonJS,因此我们在 Node.js 中经常看到:

constfs=require('fs');module.exports={foo:'bar'};

它主要通过:

require()module.exports exports

这些运行时机制实现模块化。

ESM

ESM 是 ECMAScript 官方标准中的模块系统。

它通过语言层面的模块语法实现:

importfoofrom'./foo.js';exportdefaultfoo;

或者:

exportconstname='Tom';exportfunctionsayHello(){}

因此:

CommonJS ↓ 社区模块方案 ↓ 主要通过运行时 API 实现 ESM ↓ ECMAScript 官方语言标准 ↓ 通过语言级 import / export 语法实现

4. CommonJS 和 ESM 在语法上有什么区别?

核心思路

CommonJS 使用运行时函数和对象完成模块导入导出;ESM 使用语言级的 import/export 语法。

CommonJS

// math.jsfunctionadd(a,b){returna+b;}module.exports={add};

导入:

const{add}=require('./math.js');console.log(add(1,2));

ESM

// math.jsexportfunctionadd(a,b){returna+b;}

导入:

import{add}from'./math.js';console.log(add(1,2));

核心区别:

CommonJS ↓ require() module.exports exports ESM ↓ import export

5. 为什么 CommonJS 的 require() 是运行时加载,而 ESM 的 import 是静态模块结构?

这是这道题最重要的原理。

核心思路

CommonJS 的 require() 本质上是运行时执行的函数调用,因此依赖关系可以根据运行时条件决定;ESM 的静态 import 属于模块语法,JavaScript 引擎可以在执行模块代码之前分析模块依赖。


CommonJS:运行时确定依赖

例如:

letmoduleName;if(process.env.NODE_ENV==='development'){moduleName='./dev.js';}else{moduleName='./prod.js';}constconfig=require(moduleName);

这里require()的参数可以来自变量。

因为:

代码开始执行 ↓ 判断条件 ↓ 得到 moduleName ↓ 执行 require() ↓ 确定真正需要加载的模块

所以 CommonJS 的依赖关系可以在运行过程中确定。

甚至可以:

if(condition){constfoo=require('./foo.js');}

ESM:静态 import

ESM 的静态导入:

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';constmodule=awaitimport(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 模块具有缓存机制。

例如:

consta=require('./foo');constb=require('./foo');console.log(a===b);

通常会得到:

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(this===module.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){returna+b;}exportfunctionsubtract(a,b){returna-b;}

如果:

import{add}from'./utils.js';console.log(add(1,2));

构建工具可以比较明确地知道:

utils.js ├── add ← 使用 └── subtract ← 未使用

因此可以尝试删除:

subtract()

相关代码。这就是 Tree Shaking 的基础之一。


CommonJS 的问题

例如:

constmoduleName=getModuleName();constutils=require(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 → A

CommonJS

CommonJS 执行模块时,如果发现模块正在加载,可以从缓存中拿到当前模块的部分导出结果。因此循环依赖场景下可能拿到:

{}

或者拿到一个尚未完成初始化的导出对象。

例如:

// a.jsconstb=require('./b');module.exports.name='A';console.log(b.name);
// b.jsconsta=require('./a');module.exports.name='B';console.log(a.name);

这里就可能出现:

A 加载 ↓ A require B ↓ B 加载 ↓ B require A ↓ A 尚未执行完 ↓ B 得到 A 当前已经导出的内容

因此 CommonJS 循环依赖要特别注意模块初始化顺序。


ESM

ESM 在模块执行之前,会先建立模块之间的依赖关系。

大致:

解析模块 ↓ 建立模块依赖图 ↓ 模块链接 ↓ 确定导入导出关系 ↓ 按照规范执行模块

ESM 的导入通常不是简单地把一个值复制过来,而是建立与导出绑定的关系。因此它能够更系统地处理循环依赖。

但是:

ESM 并不是“有循环依赖就一定没问题”。

如果循环依赖导致模块在变量初始化之前访问它,仍然可能出现:

ReferenceError

所以面试中不要说:

“ESM 可以完美解决循环依赖。”

正确说法是:

ESM 有更明确的模块链接和绑定机制,可以规范化处理循环依赖,但如果在模块初始化完成前访问相关绑定,仍然可能报错。


13. CommonJS 和 ESM 的主要区别总结

对比维度CommonJSECMAScript Modules(ESM)
标准来源社区规范ECMAScript 官方标准
典型环境Node.js浏览器、Node.js 等
导入require()import
导出module.exports/exportsexport
静态模块结构不属于其核心设计是核心设计
依赖确定运行时静态 import 可提前分析
动态加载require(variable)等import()
动态 import本身就是运行时调用import()返回 Promise
顶层 this(Node.js)通常为module.exportsundefined
Tree Shaking天然不利于静态分析更适合静态分析
循环依赖可能拿到未完成初始化的导出有模块链接机制,但初始化顺序仍重要
模块包装Node.js CommonJS 有函数包装ESM 不采用 CommonJS 那种函数包装

14. 使用场景

CommonJS 适合

① Node.js 旧项目

例如:

constexpress=require('express');

大量传统 Node.js 项目仍然存在 CommonJS 代码。

② 需要运行时决定加载模块

例如:

constadapter=require(`./adapters/${type}.js`);

但实际工程中如果需要动态加载,现代 ESM 通常更推荐:

constadapter=awaitimport(`./adapters/${type}.js`);

ESM 适合

① 现代前端项目

importReactfrom'react';import{createRoot}from'react-dom/client';

② 需要 Tree Shaking

import{debounce}from'./utils.js';

③ 需要明确模块依赖关系

入口模块 ↓ 模块 A ↓ 模块 B ↓ 模块 C

构建工具可以根据静态 import/export 构建完整依赖图。

④ Node.js 新项目

Node.js 现在也支持 ESM,因此不能再简单地认为:

CommonJS = Node.js ESM = 浏览器

正确理解应该是:

Node.js ├── CommonJS └── ESM 浏览器 └── ESM

15. 边界场景: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. 一个完整示例

CommonJS

math.js

// CommonJS 使用 module.exports 导出模块成员。functionadd(a,b){returna+b;}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));// 5

ESM

math.js

// ESM 使用 export 导出模块成员。exportfunctionadd(a,b){returna+b;}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));// 5

ESM 动态导入

如果需要根据运行时条件决定加载哪个模块:

asyncfunctionloadModule(type){// type 的值只有运行时才能确定。constmodulePath=type==='dev'?'./dev.js':'./prod.js';// import() 是动态导入。// 它在运行时加载模块,并返回 Promise。constmodule=awaitimport(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 模块中:

this===module.exports

通常为true。

ESM 顶层:

this===undefined

错误五:ESM 可以解决所有循环依赖问题

错误。

正确:

ESM 有规范化的模块链接和绑定机制,可以处理循环依赖,但如果循环依赖导致在初始化完成之前访问绑定,仍然可能报错。


错误六:Tree Shaking 只能支持 ESM,CommonJS 永远不能

说得太绝对。

正确:

ESM 的静态结构天然适合 Tree Shaking;CommonJS 的动态特性不利于静态分析,但构建工具可以对部分 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.js,ESM 是浏览器”。

一句话总结:CommonJS 更偏运行时模块加载,ESM 更偏静态模块结构;真正的核心区别是模块依赖什么时候能够被确定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询