# 详细解释一下CommonJS和UMD

好,这两个都是 JS 模块化历史 里的关键角色。我按"为什么会出现 → 怎么用 → 内部原理 → 现在还要不要用"来讲,不啰嗦。


# 一、CommonJS(CJS)

# 1. 为什么出现

2009 年之前,JS 没有官方的模块系统。浏览器靠 <script>标签顺序加载,全局变量满天飞,命名冲突是常态。

Ryan Dahl 做Node.js 时,需要一个服务端可用的模块规范,于是社区制定了 CommonJS(最早叫 ServerJS)。

一句话:CommonJS 是为服务端设计的同步模块规范。


# 2. 基本语法

// math.js — 定义模块
const add = (a, b) => a + b;
const sub = (a, b) => a - b;

module.exports = { add, sub };
// 或者 exports.add = add; exports.sub = sub;

// main.js — 消费模块
const math = require('./math');
console.log(math.add(1, 2)); // 3

// 或者解构
const { add, sub } = require('./math');
关键字 作用
require() 同步加载并执行模块,返回 module.exports
module.exports 模块对外暴露的对象(唯一出口)
exports module.exports的快捷引用(不能直接赋值覆盖)

# 3. 核心特征(面试常考)

特征 说明
同步加载 require()是阻塞的——加载完才执行下一行
运行时加载 require在代码运行时才解析路径、读文件
值拷贝(浅拷贝) 导出的是值的副本,不是引用
单例缓存 同一个模块 require多次,只执行一次,后续直接返回缓存
循环依赖有解 Node 会返回"未完全加载完"的半成品对象,不会死循环

# ⚠️ exportsvs module.exports的坑

// ❌ 错误:exports 被重新赋值,跟 module.exports 断开了联系
exports = { add };  // 外界拿不到 add

// ✅ 正确:往 exports 上挂属性
exports.add = add;

// ✅ 正确:直接覆盖 module.exports
module.exports = { add };

原因:exports只是 module.exports的一个引用变量,重新赋值只是让 exports指向新对象,module.exports没变。


# 4. 循环依赖是怎么解决的

// a.js
console.log('a start');
exports.done = false;
const b = require('./b');
console.log('a end, b.done =', b.done);
exports.done = true;

// b.js
console.log('b start');
exports.done = false;
const a = require('./a');  // ← a 还没执行完,但 Node 已经把 a 的 exports 对象缓存了
console.log('b end, a.done =', a.done); // false(半成品)
exports.done = true;

Node 的做法:模块没执行完就缓存了 module.exports的初始空对象 {},后续代码继续跑,最后填充完整。所以循环依赖不会死循环,但拿到的是"部分完成"的值。


# 5. 内部实现原理(简化版)

Node.js 内部大致是这样加载一个 CJS 模块的:

const moduleCache = {};

function require(id) {
  // 1. 已缓存 → 直接返回
  if (moduleCache[id]) return moduleCache[id].exports;

  // 2. 创建模块对象
  const module = { exports: {} };
  moduleCache[id] = module;

  // 3. 读文件、包装、执行
  const code = fs.readFileSync(resolvePath(id), 'utf-8');
  const wrapper = `(function(require, exports, module) {\n${code}\n})`;
  const fn = vm.runInThisContext(wrapper);
  fn(require, module.exports, module); // ← 你的代码在这里跑

  // 4. 返回 exports
  return module.exports;
}

这就是为什么你能直接用 require、exports、module——它们是函数参数注入的,不是全局变量。


# 6. CommonJS 的致命弱点

弱点 为什么是问题
同步加载 浏览器里网络 I/O 是异步的,require()会阻塞渲染
运行时才能确定依赖树 打包工具无法做 tree-shaking
顶层 this指向 module.exports 跟 ES Module 的顶层 this=undefined不一致
动态 require()无法静态分析 打包器很难知道你到底引入了哪些模块

# 二、UMD(Universal Module Definition)

# 1. 为什么出现

2010~2014 年这段时间,前端模块化群雄割据:

环境 用的规范
Node.js CommonJS (require)
浏览器(RequireJS) AMD (define)
浏览器(直接 <script>) 全局变量挂载

如果你写了一个开源库,想让它在所有环境都能用,你就得写三套代码——这显然不合理。

UMD 就是为了解决这个问题诞生的:一套代码,三种环境通吃。


# 2. UMD 长什么样

(function (root, factory) {
  // 情况 1:AMD(RequireJS / 浏览器异步加载)
  if (typeof define === 'function' && define.amd) {
    define(['dependency'], factory);

  // 情况 2:CommonJS(Node.js / Browserify)
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('dependency'));

  // 情况 3:全局变量(浏览器直接 <script> 引入)
  } else {
    root.MyLibrary = factory(root.Dependency);
  }
})(typeof self !== 'undefined' ? self : this, function (dependency) {

  // ===== 你的库代码 =====
  function hello() { return 'Hello!'; }

  return { hello };
});

# 逐行解读

代码段 干什么
(function(root, factory) { ... })(...) 立即执行函数(IIFE),防止污染全局
typeof define === 'function' && define.amd 检测是不是 RequireJS 环境
typeof module === 'object' && module.exports 检测是不是 Node/Browserify 环境
root.MyLibrary = factory(...) 兜底:浏览器 <script>直接引,挂到 window上
factory函数 真正的库逻辑,接收一个依赖参数,返回对外 API

# 3. 编译后的实际效果

假设你在浏览器里直接 <script src="mylib.umd.js">:

<script src="dependency.js"></script>
<script src="mylib.umd.js"></script>
<script>
  MyLibrary.hello(); // 'Hello!' — 挂在 window 上
</script>

在 Node.js 里:

const lib = require('mylib'); // factory 被调用,返回 { hello }
lib.hello();

在 RequireJS 里:

require(['mylib'], function(lib) {
  lib.hello();
});

# 4. UMD 的变体

# 无依赖的简化版(你之前项目里可能见过)

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory();
  } else {
    root.MyLibrary = factory();
  }
})(this, function () {
  return { hello: 'world' };
});

# jQuery 当年用的 UMD(经典)

(function (global, factory) {
  if (typeof module === 'object' && module.exports) {
    module.exports = factory(global, true);
  } else {
    factory(global);
  }
})(typeof window !== 'undefined' ? window : this, function (window, noGlobal) {
  // jQuery 本体
  return jQuery;
});

# 5. UMD 的问题

问题 说明
样板代码又臭又长 每个文件都要写一遍那个 IIFE 判断
不支持 ES Module 的 import/export语法 只是"兼容层",不是新标准
无法 tree-shake 跟 CJS 一样是运行时加载
依赖声明藏在函数体里 打包工具只能猜测依赖关系

# 三、CommonJS vs UMD vs ESM 全景对比

维度 CommonJS UMD ES Module
提出者 社区(Kevin Dangoor) 社区共识 TC39(JS 官方标准)
年份 2009 ~2011 2015(ES6)
加载方式 同步 取决于环境 异步(浏览器)/ 同步(Node)
静态分析 ❌ 不行 ❌ 不行 ✅ 可以(tree-shaking)
浏览器原生支持 ❌ ✅(全局模式) ✅(<script type="module">)
Node.js 支持 ✅ 原生 ✅ 通过打包 ✅(.mjs/ "type":"module")
循环依赖 返回半成品对象 同 CJS ✅ 更好的支持(live binding)
现在还用吗 ✅ Node 生态主力 ⚠️ 老库兼容 ✅ 新项目首选

# 四、你现在应该怎么选

场景 用什么
新项目(前端/全栈) ES Module(import/export)
Node.js 后端维护老代码 CommonJS 继续用,新文件可混用 ESM
写开源库要兼容所有人 构建时输出 双产物:dist/lib.esm.js+ dist/lib.cjs.js,package.json配 "module"+ "main"
写浏览器直接引用的 SDK 输出 UMD 或 ESM,UMD 做兜底
魔笔/低代码平台 通常跑在 Node 环境 → CommonJS

# 五、一个常见面试题

CommonJS 和 ES Module 最大的区别是什么?

区别点 CommonJS ES Module
加载时机 运行时 编译时(静态)
导出的值 值拷贝(副本) 活绑定(引用)
import位置 可以写在条件里 必须顶层
require可以动态拼路径 ✅ require('./' + name) ❌ import()可以但返回 Promise
Tree-shaking ❌ ✅
// CJS:导出的是值的快照
let count = 0;
setTimeout(() => count = 1, 100);
module.exports = { count };
// 消费者拿到的是 { count: 0 },永远不会变成 1

// ESM:导出的是活绑定
export let count = 0;
setTimeout(() => count = 1, 100);
// 消费者 import { count } 后,100ms 后 count 变成 1 ✅