博客 / 詳情

返回

一文讀懂:CommonJS 和 ES Module 的本質區別

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

面試官:你能説説 CommonJS 和 ES Module 的區別嗎?
我:……(腦子裏只剩下 requireimport

説實話,這個問題你一定見過,而且99% 的前端都背過標準答案
但真要往深了問一句:

  1. 為什麼 ESM 可以 Tree Shaking?CommonJS 不行
  2. 為什麼 ESM 的 import 是“只讀的”?

很多人,當場就開始“CPU 過載”。

於是我決定直接把底層邏輯捋清楚,以下就是我對 CommonJS 和 ES Module 一次系統性深挖的記錄

一、什麼是 CommonJS?它解決了什麼問題?

1. CommonJS 的誕生背景

在早期 JavaScript 只有瀏覽器環境時,是沒有模塊系統的

  • 全局變量污染
  • 文件之間依賴混亂
  • 無法複用代碼

於是 Node.js 社區提出了一套解決方案:CommonJS(CMJ)

👉 注意:CommonJS 是社區標準,不是官方語言層面的規範。

CommonJS 的核心特徵

  • ✅ 社區標準
  • ✅ 使用函數實現(require
  • ✅ 僅 Node 環境支持
  • ✅ 動態依賴,同步執行

2. CommonJS 為什麼叫“動態依賴”?

來看一段最典型的代碼:

const moduleName = './a.js';
const a = require(moduleName);

這裏的依賴路徑,是不是運行時才能確定?這就是動態依賴;

CommonJS 的依賴關係,必須等代碼執行時才能知道。


3. require 到底做了什麼?(核心原理)

你在 Node 中寫的:

const a = require('./a.js');

但如果我追問一句:require 加載的模塊代碼,是“直接執行”的嗎? 模塊裏的 this、exports、module.exports 到底從哪來的?

答案其實藏在 Node.js 對模塊的一層“函數包裝”裏:

function require(path) {
   const cache = {}
  // 1. 如果模塊已經加載過,直接返回緩存
  if (cache[path]) {
    return cache[path].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[modulePath] = module;
  return module.exports;
}

假設你有一個文件 a.js,那麼文件中的內容會放到上面的_run函數中執行

我們拆開來看:

ScreenShot_2026-02-27_141311_499


在模塊初始化階段,這三個引用的是同一個對象。

所以以下判斷永遠成立:

console.log(arguments); // [exports, require ,module, __filename, __dirname]
console.log(this); // {}
console.log(this === exports); // true
console.log(exports === module.exports); // true

重點來了

  • require 是一個普通函數
  • module.exports 是一個普通對象
  • 模塊執行是同步的
  • 導出的值是一次性的值拷貝

二、ES Module:語言層面的模塊系統

如果説 CommonJS 是“工具方案”,那麼 ES Module(ESM)就是 JavaScript 官方給出的答案

ES Module 的核心關鍵

  • ✅ 官方標準(ECMAScript)
  • ✅ 使用新語法(import / export
  • 所有環境支持(瀏覽器 / Node / Deno)
  • ✅ 同時支持靜態依賴 & 動態依賴
  • 符號綁定

1. 什麼是「靜態依賴」?

import { a } from './a.js';

這行代碼有兩個關鍵點:

  1. import只能寫在頂層
  2. 依賴路徑在代碼運行前就確定

👉 這意味着什麼?

  • 構建工具在編譯階段就能分析依賴
  • 支持Tree Shaking
  • 可以做代碼分割、預加載

這也是為什麼 ESM 更適合前端工程化


2. ESM 也支持動態依賴,但它是異步的

import('./a.js').then(module => {
  console.log(module.a);
});

和 CommonJS 最大的不同點:

ScreenShot_2026-02-27_141425_020

3. 符號綁定:ESM 最容易被忽略

這是 ESM 和 CommonJS 的本質區別。

看一段代碼

// a.js
export var a = 1;
export function changeA() {
  a = 2;
}
// index.js
import { a, changeA } from './a.js';
console.log(a); // 1
changeA();
console.log(a); // 2

這裏為什麼 a 會跟着變化?

真相就是 import 不是賦值,而是“引用同一個符號”

在 ESM 中: 導入的不是值,而是對導出符號的實時綁定

可以理解為:

  • a 在模塊內部是一個變量
  • 所有 import 的地方,都指向同一個 a
  • 修改它,所有地方同步變化

這就是「符號綁定(Live Binding)」。


對比 CommonJS(非常關鍵)

// 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

這裏的 n

  • 是一次值拷貝
  • 後續模塊內部怎麼改,外面都不會同步

4. 再看下 下面幾個問題

(1) export 和 export default 的區別

  • export:具名導出,可多個
  • export default:默認導出,只能一個
  • 默認導出本質是 { default: xxx }

(2) 下面代碼導出了什麼?

exports.a = 'a';
module.exports.b = 'b';
this.c = 'c';
module.exports = {
  d: 'd'
};
結果:
{ d: 'd' }
(3)下面代碼導出了什麼?
exports.a = 1;  
exports = { b: 2 };

結果:

{ a: 1 }

原因是:

  • exports 只是 module.exports 的一個引用
  • 當你重新給exports賦值時,只是斷開了引用關係module.exports 並沒有變
  • 等價於 let exports = module.exports; exports = {}; 只是改了局部變量

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.