012-ESModule与CommonJS模块比较

ES Module 与 CommonJS 的核心差异

1. 语法与设计理念

  • CommonJS (CJS)

    • 语法:require() 导入,module.exportsexports 导出。
    • 设计目标:服务端模块化(如 Node.js),同步加载,运行时解析依赖。
      1
      2
      3
      4
      // 导出
      module.exports = { key: 'value' };
      // 导入
      const obj = require('./module');
  • ES Module (ESM)

    • 语法:import 导入,export 导出。
    • 设计目标:浏览器与跨平台,静态编译(编译时分析依赖),支持异步加载(如 import())。
      1
      2
      3
      4
      // 导出
      export const key = 'value';
      // 导入
      import { key } from './module.mjs';

2. 加载机制与值传递

特性 CommonJS ES Module
加载时机 运行时同步加载 编译时解析,运行时执行
值传递 导出值的拷贝 导出值的引用(动态绑定)
循环依赖处理 通过缓存避免死循环,可能输出未完成状态的值 引用未初始化变量可能为 undefined(静态分析报错优先)
作用域 模块级作用域(通过函数包裹) 原生词法作用域

3. 异步支持与适用场景

  • CJS:仅同步加载,适用于服务端(文件读取快),浏览器端易阻塞渲染。
  • ESM:支持异步加载(import()),适用于浏览器与现代 Node.js,支持按需加载和 Tree Shaking。

4. 其他关键差异

  • 顶层 this 指向:CJS 指向当前模块,ESM 指向 undefined
  • 动态表达式:CJS 的 require() 可动态调用(如 require(path)),ESM 的 import 路径需静态字符串(动态路径需 import())。

互相加载的方式

1. ESM 加载 CommonJS

  • 默认行为:ESM 的 import 会将 CJS 的 module.exports 视为 default 导出。
    1
    2
    3
    4
    5
    // CJS 模块 (math.cjs)
    module.exports = { add: (a, b) => a + b };

    // ESM 导入
    import math from './math.cjs'; // math = { add: [Function] }
  • 命名导入的限制
    需通过解构从 default 中提取,直接命名导入会报错:
    1
    2
    3
    4
    5
    6
    // 错误!
    import { add } from './math.cjs';

    // 正确:解构 default
    import cjsModule from './math.cjs';
    const { add } = cjsModule;

2. CommonJS 加载 ESM

  • 问题:CJS 的 require() 是同步的,而 ESM 需异步加载。
  • 解决方案:使用动态 import()(返回 Promise):
    1
    2
    3
    4
    5
    // CJS 中加载 ESM
    (async () => {
    const esmModule = await import('./module.mjs');
    console.log(esmModule.key);
    })();

3. 兼容方案

  • 统一封装:若需同时支持两种规范,可创建适配层:
    1
    2
    3
    // 适配 CJS 模块为 ESM (wrapper.mjs)
    import cjsModule from './legacy.cjs';
    export const { propA, propB } = cjsModule;

与 AMD 的关系及差异

AMD 的角色

  • 定位:浏览器端异步加载的早期方案(如 RequireJS),解决 CJS 同步加载阻塞问题。
  • 语法define 定义模块,require 异步加载:
    1
    2
    3
    define(['dep1', 'dep2'], (dep1, dep2) => {
    return { key: dep1.value + dep2.value };
    });
  • 对比 ESM/CJS
    特性 AMD ESM CommonJS
    加载方式 异步 静态/支持异步 同步
    适用环境 浏览器 浏览器/Node.js Node.js
    依赖处理 依赖前置 静态分析 运行时解析

演进关系

AMD 是 ESM 成熟前浏览器的过渡方案,ESM 成为标准后逐渐被取代,但动态加载思想(如 import())吸收了其优点。


总结:如何选择?

  • Node.js 环境:传统库用 CommonJS,新项目首选 ESM(设置 "type": "module")。
  • 浏览器环境ES Module(原生支持 + 异步能力)。
  • 历史项目:AMD 仅需在维护旧浏览器代码时考虑。

通过 import/require 混用和动态加载,可实现渐进迁移,但需注意值传递和加载时序的差异。