一文读懂:CommonJS 和 ES Module 的本质区别
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<blockquote>
<p>面试官:你能说说 CommonJS 和 ES Module 的区别吗?<br>
我:……(脑子里只剩下 <code>require</code> 和 <code>import</code>)</p>
</blockquote>
<p>说实话,这个问题<strong>你一定见过</strong>,而且<strong>99% 的前端都背过标准答案</strong>。<br>
但真要往深了问一句:</p>
<ol>
<li><strong>为什么 ESM 可以 Tree Shaking?CommonJS 不行</strong></li>
<li><strong>为什么 ESM 的 import 是“只读的”?</strong></li>
</ol>
<p>很多人,当场就开始“CPU 过载”。</p>
<p>于是我决定直接把底层逻辑捋清楚,以下就是我对 <strong>CommonJS 和 ES Module 一次系统性深挖的记录</strong>。</p>
<h2 data-id="heading-0">一、什么是 CommonJS?它解决了什么问题?</h2>
<h3 data-id="heading-1">1. CommonJS 的诞生背景</h3>
<p>在早期 JavaScript 只有浏览器环境时,是<strong>没有模块系统的</strong>:</p>
<ul>
<li>全局变量污染</li>
<li>文件之间依赖混乱</li>
<li>无法复用代码</li>
</ul>
<p>于是 <strong>Node.js 社区</strong>提出了一套解决方案:<strong>CommonJS(CMJ)</strong> 。</p>
<p>👉 <strong>注意</strong>:CommonJS 是<strong>社区标准</strong>,不是官方语言层面的规范。</p>
<p><strong>CommonJS 的核心特征</strong></p>
<ul>
<li>✅ 社区标准</li>
<li>✅ 使用函数实现(<code>require</code>)</li>
<li>✅ 仅 Node 环境支持</li>
<li>✅ 动态依赖,同步执行</li>
</ul>
<hr>
<h3 data-id="heading-2">2. CommonJS 为什么叫“动态依赖”?</h3>
<p>来看一段最典型的代码:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const moduleName = './a.js';
const a = require(moduleName);</pre>
</div>
<p>这里的依赖路径,是不是运行时才能确定?这就是动态依赖;</p>
<p>CommonJS 的依赖关系,必须等代码执行时才能知道。</p>
<hr>
<h3 data-id="heading-3">3. require 到底做了什么?(核心原理)</h3>
<p>你在 Node 中写的:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const a = require('./a.js');</pre>
</div>
<p>但如果我追问一句:require 加载的模块代码,是“直接执行”的吗? 模块里的 this、exports、module.exports 到底从哪来的?</p>
<p>答案其实藏在 Node.js 对模块的一层“函数包装”里:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">function require(path) {
const cache = {}
// 1. 如果模块已经加载过,直接返回缓存
if (cache) {
return cache.exports;
}
// 2. 创建模块对象
const module = {
id:path
exports: {}
};
// 3. 执行模块代码(用函数包一层)
function _run(exports, require, module, __filename, __dirname) {
// 模块源码在这里执行
}
_run.call(
module.exports,
module.exports,
require,
module,
__filename,
__dirname
);
// 4. 缓存并返回结果
cache = module;
return module.exports;
}</pre>
</div>
<p>假设你有一个文件 <code>a.js</code>,那么文件中的内容会放到上面的<code>_run</code>函数中执行</p>
<p>我们拆开来看:</p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260227141332713-405235955.png" alt="ScreenShot_2026-02-27_141311_499" loading="lazy"></p>
<br>
</div>
<p>在模块初始化阶段,这三个引用的是同一个对象。</p>
<p>所以以下判断永远成立:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">console.log(arguments); //
console.log(this); // {}
console.log(this === exports); // true
console.log(exports === module.exports); // true</pre>
</div>
<div>
<div>
<p><strong>重点来了</strong>:</p>
<ul>
<li><code>require</code> 是一个<strong>普通函数</strong></li>
<li><code>module.exports</code> 是一个<strong>普通对象</strong></li>
<li>模块执行是<strong>同步的</strong></li>
<li>导出的值是一次性的<strong>值拷贝</strong></li>
</ul>
<hr>
<h2 data-id="heading-4">二、ES Module:语言层面的模块系统</h2>
<p>如果说 CommonJS 是“工具方案”,那么 <strong>ES Module(ESM)就是 JavaScript 官方给出的答案</strong>。</p>
<p><strong>ES Module 的核心关键</strong></p>
<ul>
<li>✅ 官方标准(ECMAScript)</li>
<li>✅ 使用新语法(<code>import / export</code>)</li>
<li>✅ <strong>所有环境支持</strong>(浏览器 / Node / Deno)</li>
<li>✅ 同时支持静态依赖 & 动态依赖</li>
<li>✅ <strong>符号绑定</strong></li>
</ul>
<h3 data-id="heading-5">1. 什么是「静态依赖」?</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { a } from './a.js';</pre>
</div>
<div>
<div>
<p>这行代码有两个关键点:</p>
<ol>
<li><code>import</code>只能写在顶层</li>
<li>依赖路径在代码运行前就确定</li>
</ol>
<p>👉 <strong>这意味着什么?</strong></p>
<ul>
<li>构建工具在<strong>编译阶段</strong>就能分析依赖</li>
<li>支持Tree Shaking</li>
<li>可以做代码分割、预加载</li>
</ul>
<p>这也是为什么 <strong>ESM 更适合前端工程化</strong>。</p>
<hr>
<h3 data-id="heading-6">2. ESM 也支持动态依赖,但它是异步的</h3>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import('./a.js').then(module => {
console.log(module.a);
});</pre>
</div>
<p>和 CommonJS 最大的不同点:</p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260227141428170-372815091.png" alt="ScreenShot_2026-02-27_141425_020" loading="lazy"></p>
<h3 data-id="heading-7">3. 符号绑定:ESM 最容易被忽略</h3>
<p>这是 ESM 和 CommonJS 的本质区别。</p>
<p>看一段代码</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// a.js
export var a = 1;
export function changeA() {
a = 2;
}</pre>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// index.js
import { a, changeA } from './a.js';
console.log(a); // 1
changeA();
console.log(a); // 2</pre>
</div>
</div>
<div>
<div>
<p>这里为什么 a 会跟着变化?</p>
<blockquote>
<p>真相就是 import 不是赋值,而是“引用同一个符号”</p>
<p>在 ESM 中: <strong>导入的不是值,而是对导出符号的实时绑定</strong></p>
</blockquote>
<p>可以理解为:</p>
<ul>
<li><code>a</code> 在模块内部是一个变量</li>
<li>所有 import 的地方,都指向同一个 a</li>
<li>修改它,所有地方同步变化</li>
</ul>
<p>这就是「<strong>符号绑定(Live Binding)</strong>」。</p>
<hr>
<p><strong>对比 CommonJS(非常关键)</strong></p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// a.js
var n = 1;
function changeN() {
n = 2;
}
module.exports = {
n,
changeN
}
// b.js
const { n, changeN } = require('./a.js');
console.log(n); // 1
changeN();
console.log(n); // 1</pre>
</div>
<div>
<div>
<p>这里的 <code>n</code>:</p>
<ul>
<li>是一次<strong>值拷贝</strong></li>
<li>后续模块内部怎么改,外面都<strong>不会同步</strong></li>
</ul>
<hr>
<h3 data-id="heading-8">4. 再看下 下面几个问题</h3>
<p><strong>(1) export 和 export default 的区别</strong></p>
<ul>
<li><code>export</code>:具名导出,可多个</li>
<li><code>export default</code>:默认导出,只能一个</li>
<li>默认导出本质是 <code>{ default: xxx }</code></li>
</ul>
<p><strong>(2) 下面代码导出了什么?</strong></p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">exports.a = 'a';
module.exports.b = 'b';
this.c = 'c';
module.exports = {
d: 'd'
};</pre>
</div>
结果:<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">{ d: 'd' }</pre>
</div>
</div>
<div>(3)下面代码导出了什么?<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">exports.a = 1;
exports = { b: 2 };</pre>
</div>
<p>结果:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">{ a: 1 }</pre>
</div>
<div>
<div>
<p>原因是:</p>
<ul>
<li><code>exports</code> 只是 <code>module.exports</code> 的一个<strong>引用</strong></li>
<li>当你重新给<code>exports</code>赋值时,只是断开了引用关系<code>module.exports</code> 并没有变</li>
<li>等价于 let exports = module.exports; exports = {}; 只是改了局部变量</li>
</ul>
</div>
<div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19646596
頁:
[1]