JavaScript CommonJS require 与 ES Module import 的区别
深入对比 JavaScript 中 CommonJS 的 require 与 ES Module 的 import 的核心机制差异,包括加载时机、值拷贝与实时绑定、动态引用、作用域上下文及工程实践中的混用与选型。
require 是运行时同步加载,导入的是导出值的浅拷贝与缓存对象;而 ES Module 的 import 采用静态解析机制,在代码执行前完成依赖图构建,导入的是对模块导出符号的只读实时绑定(Live Binding)。理解这两种机制在解析时机、数据引用与作用域环境上的差异,是解决现代工程构建报错与模块混用问题的关键。
一、基础语法与调用形式对比
在语法表现上,CommonJS(简称 CJS)最初由 Node.js 广泛推广,而 ES Module(简称 ESM)则是 ECMAScript 官方引入的标准语法规范。
CommonJS 依赖全局注入的 require 函数和 module.exports(或简写的 exports)对象实现模块导出与引入:
// math-cjs.js (CommonJS 导出)
function add(a, b) {
return a + b;
}
module.exports = { add };
// main-cjs.js (CommonJS 导入)
const { add } = require('./math-cjs.js');
console.log(add(2, 3));
ES Module 则使用专有的关键字 export 和 import:
// math-esm.js (ES Module 导出)
export function add(a, b) {
return a + b;
}
// main-esm.js (ES Module 导入)
import { add } from './math-esm.js';
console.log(add(2, 3));
从语义上看,require() 是一个可以直接在任何表达式中调用的普通 JavaScript 函数;而静态 import 属于语言级别的关键字声明,必须位于当前文件的顶层作用域,不能直接包裹在条件判断或函数块内。
二、加载机制:运行时同步加载 vs 静态三阶段解析
CommonJS 与 ES Module 在底层的运行生命周期存在本质分歧。在 Node.js 中,执行一个 require() 调用时,解释器会立即同步中断当前脚本的执行,转而读取并执行目标脚本代码,将其导出的对象存入模块缓存(require.cache),最后返回该对象。
相比之下,ES Module 规范为了适配浏览器端异步网络请求与模块分析,将模块加载拆分为三个明确独立的阶段:
- 构建与解析(Construction / Parsing):在不执行任何实际业务代码的前提下,从入口文件开始递归扫描所有的
import语句,下载并解析为抽象语法树(AST),构建出完整的静态依赖图谱。 - 符号实例化(Instantiation):为每一个导出的变量在内存中分配专门的插槽(Bindings),并将导入方的标识符指向导出方对应的内存地址,此时变量尚未赋值。
- 求值与执行(Evaluation):按照依赖拓扑顺序,由叶子节点向根节点自底向上执行模块内部代码,将实际计算结果填入对应的内存插槽中。
由于 ES Module 在真正运行业务逻辑前就已经完全知晓依赖结构,现代前端打包工具(如 Rollup、Webpack、Vite)能够基于这种静态结构进行精确的死代码消除(Tree Shaking),剔除未被使用的导出函数。而 CommonJS 的动态特性使得打包工具极难在静态分析期安全删除未引用代码。
三、导出机制本质:值拷贝 vs 实时动态绑定 (Live Binding)
两者的另一个本质区别体现在数据的传递方式上。CommonJS 输出的是值的浅拷贝与快照,而 ES Module 输出的是对内存中变量的实时只读引用。
1. CommonJS 的值拷贝现象
在 CommonJS 中,当一个模块通过 module.exports 暴露基本数据类型变量时,外部获取到的是该变量当前状态的一份拷贝。如果模块内部后续通过内部方法更改了这个变量的值,外部已经获取到的变量不会随之同步更新:
// counter-cjs.js
let count = 1;
function increment() {
count++;
}
module.exports = { count, increment };
// test-cjs.js
const { count, increment } = require('./counter-cjs.js');
console.log(count); // 输出: 1
increment();
console.log(count); // 依然输出: 1 (外部拿到的仅是导出时刻的数值拷贝)
原因在于 module.exports = { count, increment } 创建了一个普通字面量对象,将当时的 count 数值(1)赋值给对象的属性。后续调用 increment() 修改的是模块文件闭包作用域内的本地变量 count,与外部对象上的属性已完全解耦。
2. ES Module 的实时绑定 (Live Binding)
ES Module 的导入变量并非拷贝,而是直接建立与源模块符号内存地址的实时链接:
// counter-esm.js
export let count = 1;
export function increment() {
count++;
}
// test-esm.js
import { count, increment } from './counter-esm.js';
console.log(count); // 输出: 1
increment();
console.log(count); // 输出: 2 (实时反映模块内部状态的变化!)
需要特别注意的是,在导入方(test-esm.js)中,count 是一个只读绑定(Read-only Binding)。直接在外部对导入变量执行赋值(如 count = 10;)会在编译/运行阶段抛出 TypeError: Assignment to constant variable 错误。变量的修改权严格保留在定义该变量的源模块内部。
四、动态加载与条件引用的差异
在业务开发中,开发者有时需要根据环境变动、用户权限或运行时入参来动态引入不同的逻辑文件。CommonJS 与 ES Module 在处理动态依赖时的策略明显不同。
1. CommonJS 的原生动态灵活性
由于 require 是一个纯粹的执行期函数,它可以在任何控制流语句中无缝运行,甚至参数本身也可以是动态拼接的字符串:
// 运行时根据环境变量动态读取
let logger;
if (process.env.NODE_ENV === 'production') {
logger = require('./loggers/prodLogger.js');
} else {
logger = require('./loggers/devLogger.js');
}
// 动态拼接文件路径
const pluginName = 'analytics';
const plugin = require(`./plugins/${pluginName}.js`);
2. ES Module 的顶层限制与动态 import()
标准的静态 import ... from '...' 不允许嵌套在 if 分支或函数体内部,否则会在代码词法解析阶段直接报语法错误(SyntaxError)。
为了满足按需加载与代码分割(Code Splitting)的需求,ES 规范引入了函数形式的 import(specifier) 动态操作符。与 CommonJS 的同步返回不同,import() 会返回一个 Promise 对象,必须通过 await 或 .then() 获取导出内容:
// ES Module 中的动态条件加载
let logger;
if (process.env.NODE_ENV === 'production') {
const prodMod = await import('./loggers/prodLogger.js');
logger = prodMod.default;
} else {
const devMod = await import('./loggers/devLogger.js');
logger = devMod.default;
}
五、作用域与执行上下文差异
由于两者的运行宿主和加载包装方式不同,它们在上下文变量和关键字的指向细节上也存在若干常见差异:
-
顶层
this指向:在 CommonJS 模块的顶层作用域中,this指向当前模块的exports对象;而在严格模式下的 ES Module 顶层作用域中,this为undefined。 -
路径内置变量:CommonJS 模块在运行时会被 Node.js 包装为一个函数入参(包括
exports, require, module, __filename, __dirname),因此可直接访问__dirname和__filename。ES Module 则不再注入这些变量,需要通过import.meta.url配合 Node.js 的url模块进行获取:// 在 ES Module 中获取当前目录与文件路径 import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -
顶层 Await(Top-Level Await):在现代 Node.js 与主流浏览器支持的 ES Module 中,可以直接在模块顶层使用
await关键字进行异步初始化;而 CommonJS 模块的同步包装函数默认不支持直接编写顶层await。
六、核心特性综合决策矩阵
为了便于读者快速查阅与建立系统化的知识网络,以下矩阵将 CommonJS 与 ES Module 在核心维度的技术特性进行了对照归纳:
七、Node.js 中的混用与现代工程实践
在现阶段的 Node.js 工程中,常常需要面临两套规范并存的现状。掌握两者的互操作规则可以有效避免运行时报错:
1. Node.js 对 ESM 模式的判定方式
Node.js 默认将 .js 文件视为 CommonJS 模块。如果要启用 ES Module 解析,可以通过以下两种方式:
- 在
package.json中明确声明"type": "module",此时当前项目下所有的.js文件均按照 ESM 解析。 - 将模块文件后缀命名为
.mjs(显式声明为 ESM)或.cjs(显式声明为 CommonJS)。
2. 互操作关键规则
在 ESM 模块中可以使用 import package from 'cjs-module' 引入 CommonJS 库。此时 CommonJS 的 module.exports 整体会被包裹为 ESM 的 default 导出对象。
require() 引入纯 ESM 模块
如果尝试在 CommonJS 文件中使用 require('./esm-file.mjs'),Node.js 会抛出 ERR_REQUIRE_ESM 错误。若必须在 CJS 中加载纯 ESM 模块,唯一的合规做法是使用异步函数:const esmMod = await import('./esm-file.mjs');。
3. 选型建议与总结
现代浏览器原生已全面支持 ES Module,同时全生命周期的新版主流 NPM 库(如 lodash-es、chalk、node-fetch 等)均在积极转向 ESM-only。在搭建新的前端工程或现代 Node.js 服务端应用时,优先采用 ES Module 作为全项目的基准规范,以便充分发挥静态检查、更小打包体积(Tree Shaking)和平台通用性的核心优势。而在维护既有老旧服务端基础设施或传统自动化脚本时,保留 CommonJS 则能兼顾更低的历史迁移成本。
NestJS 是什么?与 Express、Koa 的核心区别及选型建议
2026-11-05 13:46:41
生产环境升级 Node.js 的注意事项与兼容性测试策略
2026-11-05 09:19:24
nvm 常用命令详解:怎么平滑切换和升级 Node.js 版本
2026-11-04 09:02:34
JavaScript和JSP的根本区别:运行环境与执行机制解析
2026-11-03 23:30:18
使用nvm卸载指定版本Node.js并切换默认环境
2026-11-03 23:09:26








