Skip to content

JavaScript 模块化深度解析:CJS 与 ESM

在现代 JavaScript 开发中,模块化是组织代码、避免命名空间污染和提高可复用性的核心手段。目前主流的两种规范是 CommonJS (CJS)ES Modules (ESM)


1. CommonJS (CJS)

CJS 是 Node.js 默认使用的模块系统(早期版本唯一支持)。

核心语法

javascript
// 导出 lib.js
const name = 'CommonJS';
module.exports = { name };

// 导入 main.js
const lib = require('./lib.js');
console.log(lib.name);

特点

  • 运行时加载:模块在代码运行时同步加载。
  • 值拷贝:导出的值是原始值的拷贝。一旦导出,模块内部的变化不会影响已经导出的值。
  • 同步执行:由于主要用于服务端,读取磁盘速度较快,同步加载不会阻塞进程。

2. ES Modules (ESM)

ESM 是 ECMAScript 官方提出的标准,也是浏览器原生支持的唯一模块规范。

核心语法

javascript
// 导出 lib.js
export const name = 'ESModule';
export default function sayHi() {}

// 导入 main.js
import { name } from './lib.js';
import sayHi from './lib.js';

特点

  • 静态编译:引擎在代码执行前(链接阶段)就确定了模块间的依赖关系,允许进行 Tree-shaking 优化。
  • 实时绑定 (Live Binding):导出的是值的引用。如果模块内部修改了导出的变量,外部引用也会随之改变。
  • 异步加载:特别适合浏览器环境,不会阻塞页面渲染。

3. AMD (Asynchronous Module Definition)

AMD 是专门为浏览器端设计的模块规范,其核心特点是异步加载,模块的加载不阻塞页面的渲染。

核心语法

AMD 使用 define 函数定义模块,使用 require 函数加载模块。

javascript
// 定义模块 (math.js)
define(['dependency'], function(dependency) {
  const add = (a, b) => a + b;
  return { add };
});

// 加载模块 (main.js)
require(['math'], function(math) {
  console.log(math.add(1, 2));
});

特点

  • 依赖前置:在定义模块时就需要声明所有依赖,并在回调函数中作为参数传入。
  • 异步加载:通过动态创建 <script> 标签实现异步加载,适合网络环境不稳定的浏览器端。
  • 代表库RequireJS 是 AMD 最具代表性的实现。

4. UMD (Universal Module Definition)

UMD 并不是一种新的模块规范,而是一种兼容性模式。它的目标是让同一个模块文件既能在浏览器端(通过 <script> 或 AMD 加载)运行,又能在 Node.js (CommonJS) 环境运行。

实现原理

UMD 通常通过一个自执行函数(IIFE)实现。它会检测当前环境:

  1. 如果存在 definedefine.amd 为真,则使用 AMD 方式。
  2. 如果存在 modulemodule.exports 为真,则使用 CommonJS 方式。
  3. 如果都不存在,则将模块挂载到全局变量(如 windowglobal)上。

典型代码模板

javascript
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD 环境
    define(['dependency'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS 环境
    module.exports = factory(require('dependency'));
  } else {
    // 浏览器全局变量环境
    root.myModule = factory(root.dependency);
  }
}(this, function (dependency) {
  // 模块核心逻辑
  return { name: 'UMD' };
}));

4. CJS 与 ESM 的核心区别

特性CommonJS (CJS)ES Modules (ESM)
关键字require, module.exportsimport, export
加载时机运行时 (Runtime)编译时 (Compile-time)
导出内容值的拷贝 (Value Copy)值的引用 (Live Binding)
加载方式同步异步
循环引用输出部分完成的对象,易产生 undefined建立连接,可能触发暂时性死区 (TDZ) 报错
Top-level await不支持支持
this 指向指向当前模块对象指向 undefined
严格模式默认非严格(除非指定)强制处于严格模式

4. 多种导入导出方式

4.1 命名导出与导入 (Named)

可以导出多个变量,导入时需要名称匹配。

javascript
// 导出
export const a = 1;
export const b = 2;

// 导入
import { a, b } from './module.js';

4.2 默认导出与导入 (Default)

每个模块只能有一个默认导出,导入时可以自定义名称。

javascript
// 导出
export default function() {}

// 导入
import myFunc from './module.js';

4.3 整体导入 (Namespace Import)

将模块的所有导出整合到一个对象中。

javascript
import * as Utils from './utils.js';
Utils.method1();

4.4 动态导入 (Dynamic Import)

允许按需加载模块,返回一个 Promise,适用于代码分割(Code Splitting)。

javascript
import('./heavy-module.js').then((m) => {
  m.doSomething();
});

5. 关于循环引用的差异(进阶)

这是面试中常考的一个细节:

  • CJS:当遇到循环引用时,由于是按序执行,它会返回当前已经执行完的那部分 exports 对象。如果访问尚未导出的属性,会得到 undefined
  • ESM:它先建立模块间的引用桥梁,但不求值。在执行阶段,如果你试图在变量初始化前访问它(比如 const 声明的变量),会因为 TDZ (暂时性死区) 抛出 ReferenceError

TIP

最佳实践

  1. 在现代前端项目中(Vite, Webpack 5+),优先使用 ESM
  2. 避免在模块顶层直接调用可能引发循环引用的复杂逻辑。
  3. 利用动态导入来解决某些极端的递归引用问题。