在前端開發早期,JavaScript 並沒有官方的模塊化規範——多個腳本文件通過 <script> 標籤引入時,變量會掛載到全局作用域,導致命名衝突、依賴混亂等問題。從 CommonJS 到 ES Module(ESM),JavaScript 模塊化的演進本質上是解決“代碼複用、作用域隔離、依賴管理”的過程。本文從歷史背景、核心差異到實戰應用,梳理模塊化的演進脈絡,幫你理解不同規範的設計初衷和實際用法。

一、模塊化的誕生背景:為什麼需要模塊化?

早期前端項目簡單,單個 JS 文件就能滿足需求,但隨着項目複雜度提升,兩個核心問題凸顯:

  1. 全局作用域污染:多個腳本文件的變量、函數共享 window 作用域,比如 var utils = {} 會被後續腳本覆蓋;
  2. 依賴管理混亂:腳本加載順序完全依賴 <script> 標籤順序,若 A 腳本依賴 B 腳本,B 必須寫在 A 前面,維護成本極高。

後端 Node.js 出現後,首先面臨“文件拆分、依賴加載”的問題,CommonJS 規範應運而生;而前端則在多年摸索後,最終將 ES Module 納入 ECMAScript 官方標準,形成了現在“前端 ESM + 後端 CommonJS(Node.js)”的主流格局。

二、CommonJS:Node.js 主導的模塊化方案

CommonJS 是 2009 年提出的模塊化規範,核心為 Node.js 設計,解決了服務器端的模塊化問題,特點是“同步加載、運行時加載”。

1. 核心語法

  • 導出(module.exports/exports):將模塊內的變量/函數暴露給外部;
  • 導入(require):加載其他模塊,同步獲取導出內容。
// 模塊文件:utils.js(CommonJS 導出)
// 方式1:module.exports 導出對象
module.exports = {
  sum: (a, b) => a + b,
  PI: 3.14
}

// 方式2:exports 逐個導出(語法糖,本質是 module.exports 的引用)
exports.sub = (a, b) => a - b;

// 注意:不能直接賦值 exports = {},會斷開與 module.exports 的引用
// 引入模塊:main.js(CommonJS 導入)
const utils = require('./utils.js');
// 解構導入
const { sum, PI } = require('./utils.js');

console.log(sum(1, 2)); // 3
console.log(utils.sub(5, 3)); // 2

2. CommonJS 的核心特點

  • 同步加載:require 會阻塞後續代碼執行,直到模塊加載完成,適合服務器端(文件讀取速度快),但不適合前端(網絡請求慢,會導致頁面卡頓);
  • 運行時加載:require 是運行時執行的,能動態拼接路徑(如 require('./' + fileName + '.js'));
  • 模塊緩存:模塊加載後會緩存,多次 require 同一模塊只會執行一次;
  • 全局變量:Node.js 為每個模塊提供 moduleexportsrequire 等全局變量,無需聲明即可使用。

三、ES Module:官方標準化的模塊化方案

ES Module(ESM)是 ES6(2015)引入的官方模塊化規範,兼顧前端和後端,特點是“靜態分析、異步加載”,現已成為前端模塊化的主流。

1. 核心語法

  • 導出(export):分命名導出、默認導出兩種方式;
  • 導入(import):靜態導入、動態導入(ES2020 新增)。
// 模塊文件:math.js(ESM 導出)
// 方式1:命名導出(可多個)
export const PI = 3.14;
export const multiply = (a, b) => a * b;

// 方式2:默認導出(僅一個)
export default function divide(a, b) {
  return a / b;
}

// 方式3:批量導出
const square = (x) => x * x;
export { square };
// 引入模塊:app.js(ESM 導入)
// 1. 靜態導入(編譯時確定,不可動態修改)
import divide from './math.js'; // 導入默認導出
import { PI, multiply, square } from './math.js'; // 導入命名導出
import * as math from './math.js'; // 導入所有導出,掛載到 math 對象

console.log(divide(6, 2)); // 3
console.log(math.multiply(2, 3)); // 6

// 2. 動態導入(運行時加載,返回 Promise)
const loadMathModule = async () => {
  const math = await import('./math.js');
  console.log(math.PI); // 3.14
};
loadMathModule();

2. ESM 在瀏覽器中的使用

瀏覽器中使用 ESM 需給 <script> 標籤添加 type="module"

<!-- 前端頁面引入 ESM 模塊 -->
<script type="module">
  import { PI } from './math.js';
  console.log(PI);
</script>
<!-- 兼容舊瀏覽器的回退方案 -->
<script nomodule src="./fallback.js"></script>

四、CommonJS vs ES Module:核心差異

特性

CommonJS

ES Module

加載時機

運行時加載

編譯時(靜態)加載

加載方式

同步加載

異步加載(瀏覽器)/ 同步(Node.js)

導出方式

module.exports/exports

export / export default

導入方式

require

import / import()

動態導入

支持(運行時拼接路徑)

僅 import() 支持

模塊值

導出值的拷貝

導出值的引用(響應式)

頂層 this

模塊自身(Node.js)

undefined

關鍵差異實戰:值拷貝 vs 引用

// CommonJS:值拷貝(導出後修改不影響導入方)
// counter.js
let count = 0;
module.exports = {
  count,
  add: () => count++
};

// main.js
const { count, add } = require('./counter.js');
add();
console.log(count); // 0(拷貝值,不會更新)
// ESM:值引用(導出後修改會同步到導入方)
// counter.js
export let count = 0;
export const add = () => count++;

// main.js
import { count, add } from './counter.js';
add();
console.log(count); // 1(引用值,實時更新)

五、模塊化的落地實踐

1. Node.js 中混用兩種規範

Node.js 從 v14 開始全面支持 ESM,可通過以下方式切換:

  • CommonJS 文件:後綴 .js,默認使用 CommonJS;
  • ESM 文件:後綴 .mjs,或在 package.json 中設置 "type": "module"
// package.json 配置 ESM
{
  "type": "module"
}

2. 前端工程化中的模塊化

現代前端工程(Webpack/Vite)已默認支持 ESM,即使項目中寫 CommonJS 語法,也會被打包工具轉換為瀏覽器可識別的 ESM 或兼容代碼:

// Webpack 中可混用 CommonJS 和 ESM
import React from 'react'; // ESM
const axios = require('axios'); // CommonJS

3. 模塊化的未來:統一的規範

目前 ESM 已成為前端絕對主流,Node.js 也在逐步向 ESM 遷移,CommonJS 更多用於老項目維護。兩者的核心差異正在被工具層抹平,比如:

  • Vite 優先使用 ESM,打包速度遠快於基於 CommonJS 的 Webpack;
  • Node.js 的 require 可加載 ESM 模塊(需通過 import() 轉換)。

六、避坑指南

  1. CommonJS 中 exports 賦值陷阱:不能直接給 exports 賦值(exports = {}),需用 module.exports
  2. ESM 靜態導入限制:import 路徑不能是動態變量(如 import('./' + name + '.js')),需用 import() 動態導入;
  3. 瀏覽器 ESM 跨域問題:本地開發時,ESM 模塊加載需通過 HTTP 服務(如 npx serve),不能直接打開本地文件;
  4. 值拷貝 vs 引用:CommonJS 導出基本類型是拷貝,ESM 是引用,修改導出值時需注意差異。

總結

JavaScript 模塊化的演進,是從“社區方案(CommonJS)”到“官方標準(ES Module)”的過程:

  • CommonJS 解決了 Node.js 服務器端的模塊化問題,同步加載、運行時解析的設計適配了服務器環境;
  • ES Module 兼顧前端和後端,靜態分析、異步加載的特性更適合瀏覽器,也支持更靈活的動態導入;
  • 現代開發中,前端優先使用 ESM,Node.js 可通過配置兼容兩種規範,打包工具則抹平了兩者的語法差異。

理解模塊化的演進,本質是理解“不同環境下如何優雅地管理代碼依賴和作用域”。無論是維護老項目的 CommonJS 代碼,還是開發新的 ESM 項目,掌握兩者的核心差異和用法,能讓你更清晰地組織代碼結構,避免模塊化相關的常見坑。