FumadocsZDecode
JavaScript

模块化

了解 JavaScript 各模块规范的原理、语法与适用场景

JavaScript 模块化让你将代码拆分为独立文件,通过导入/导出机制在文件间共享功能。不同的运行环境和历史背景催生了多种模块规范,了解它们的区别有助于在不同场景下做出正确选择。


一、CommonJS(CJS)

CommonJS 是 Node.js 的原生模块系统,使用同步的 require() 加载模块,文件之间通过 module.exports 共享内容。

// math.js — 导出
const add = (a, b) => a + b
const multiply = (a, b) => a * b

module.exports = { add, multiply }
// index.js — 导入
const { add, multiply } = require('./math')

console.log(add(2, 3))      // 5
console.log(multiply(2, 3)) // 6

特点:

  • 同步加载,适合服务端(磁盘 I/O 速度快)
  • module.exports 导出的是值的拷贝(对象类型为引用拷贝)
  • 每个文件拥有独立的模块作用域,不污染全局

二、ES Modules(ESM)

ES Modules 是 ECMAScript 2015 引入的官方标准,现代浏览器和 Node.js v12+ 原生支持。新项目优先使用这种格式。

// math.js — 具名导出 + 默认导出
export const add = (a, b) => a + b

export default function greet(name) {
  return `Hello, ${name}`
}
// index.js — 导入
import greet, { add } from './math.js'

console.log(add(1, 2))       // 3
console.log(greet('World'))  // Hello, World

ESM 也支持动态导入,在需要时按需加载模块:

// 点击时才加载模块(代码分割)
button.addEventListener('click', async () => {
  const { heavyUtil } = await import('./heavy-util.js')
  heavyUtil.run()
})

特点:

  • 静态分析结构,打包工具可做 Tree Shaking(移除未用代码)
  • import 绑定的是实时引用(live binding),不是值的拷贝
  • 在 Node.js 中需将文件命名为 .mjs,或在 package.json 中设置 "type": "module"

三、AMD

AMD(Asynchronous Module Definition)专为浏览器设计,依赖 RequireJS 等加载器实现异步加载,避免阻塞页面渲染。

// 定义模块(依赖 jQuery)
define(['jquery'], function ($) {
  function showMessage(msg) {
    $('body').append(`<p>${msg}</p>`)
  }

  return { showMessage }
})
// 使用模块
require(['myModule'], function (myModule) {
  myModule.showMessage('Hello, AMD')
})

特点:

  • 语法比 ESM 冗长,现已基本被 ESM 取代
  • 需要引入额外的加载器库(如 RequireJS)
  • 仍可在需要支持旧浏览器的遗留项目中看到

四、IIFE

IIFE(Immediately Invoked Function Expression,立即调用函数表达式)把模块代码包裹在一个立即执行的函数中,通过闭包隔离私有变量,避免污染全局作用域。

const MyLib = (function () {
  // _version 是私有变量,外部无法访问
  const _version = '1.0.0'

  function greet(name) {
    return `Hello, ${name} (v${_version})`
  }

  return { greet }
})()

console.log(MyLib.greet('World')) // Hello, World (v1.0.0)
console.log(MyLib._version)       // undefined

特点:

  • 无需模块加载器,直接在 <script> 标签中使用
  • 适合打包为单文件分发的库(如早期的 jQuery)
  • 多个 IIFE 之间若需共享状态,只能通过全局变量,容易出错

五、UMD

UMD(Universal Module Definition,通用模块定义)在运行时检测当前环境,自动适配 AMD、CommonJS 或全局变量三种方式。打包工具(Rollup、Webpack)在输出 UMD 格式时会自动生成这段模板代码。

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dep'], factory)                     // AMD
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('dep'))     // CommonJS
  } else {
    root.MyLib = factory(root.dep)               // 全局变量
  }
})(typeof self !== 'undefined' ? self : this, function (dep) {
  return {
    greet: (name) => `Hello, ${name}`
  }
})

特点:

  • 同时兼容浏览器和 Node.js,适合需要广泛兼容的开源库
  • 模板代码冗长,通常由打包工具自动生成,手写容易出错
  • 现代项目建议直接发布 ESM,需要兼容旧环境时再附加 UMD 输出

六、SystemJS

SystemJS 是一个动态模块加载器,支持 System.register 格式。在浏览器原生 ESM 普及之前,它是运行 ESM 语法的主要方案,常见于早期 Angular 项目。

System.register(['./dep.js'], function (_export, _context) {
  let dep

  return {
    setters: [function (d) { dep = d.default }],
    execute: function () {
      _export('greet', function (name) {
        return `Hello, ${name}`
      })
    }
  }
})

特点:

  • 支持动态导入和 URL 导入
  • 现代项目中已较少使用,原生 ESM 可满足大多数场景

七、格式对比

各格式在环境兼容性和加载方式上各有侧重,下表帮助你快速定位适合的格式。

格式运行环境加载方式适用场景
CJSNode.js同步服务端应用、旧版 Node.js 包
ESM浏览器 / Node.js异步现代前端与全栈项目(推荐)
AMD浏览器异步遗留项目(RequireJS)
IIFE浏览器同步单文件库分发
UMD通用同/异步需要兼容多环境的开源库
SystemJS浏览器异步旧版 Angular 等遗留项目

如何选择? 新项目优先使用 ESM。发布 npm 包时,用打包工具同时输出 ESM 和 CJS 格式以兼容不同消费者。需要支持旧浏览器时,选择 UMDIIFE

On this page