AMD 和 CommonJS 的区别是什么?
一句话:AMD 为浏览器设计,异步加载依赖;CommonJS 为 Node.js 设计,同步加载依赖。
这个根本差异决定了它们在语法、执行时机、输出行为上的所有不同。
核心差异对照
| AMD | CommonJS | |
|---|---|---|
| 加载方式 | 异步(网络请求不阻塞) | 同步(磁盘读取即返回) |
| 声明语法 | define(['dep'], fn) | require('dep') |
| 依赖时机 | 前置声明,并行加载后执行 | 运行时加载,顺序执行 |
| 输出类型 | 值的引用(模块内变更对外可见) | 值的拷贝(require 返回后与源模块脱钩) |
| 典型实现 | RequireJS | Node.js |
| 适用环境 | 浏览器 | 服务端 |
为什么浏览器不能同步 require
浏览器里模块要从网络加载。如果 var a = require('a') 是同步的,在 a.js 下载完成之前,主线程什么也做不了——页面直接卡死。所以 AMD 的做法是把依赖声明在数组里,加载器并行下载所有依赖,全部就绪后才执行工厂函数:
js// AMD define(['./utils', './logger'], function(utils, logger) { logger.log(utils.format('hello')); });
Node.js 没有这个问题,文件就在本地磁盘,require 同步读取后立刻返回模块对象:
js// CommonJS const utils = require('./utils'); const logger = require('./logger'); logger.log(utils.format('hello'));
输出拷贝 vs 引用
这是面试中容易被追问的细节:
js// counter.js (CommonJS) let count = 0; module.exports = { count, increment() { count++; } }; // main.js const counter = require('./counter'); counter.increment(); console.log(counter.count); // 0,不是 1
count 是导出时值的拷贝,模块内部 count++ 不会影响外部拿到的值。ESModule 的 export let count 则是实时绑定,外部能读到最新值。
循环依赖的处理差异
CommonJS 遇到循环依赖时,拿到的是已执行部分的快照,可能是不完整对象。AMD 因为是异步加载完再执行,循环依赖的模块都能拿到完整导出——前提是加载器支持。实际项目中循环依赖本身就是代码坏味道,应尽量避免。
追问
为什么 CommonJS 不能做 Tree-Shaking?
require 是运行时调用,可以写在 if 分支、循环里,打包工具无法在编译时确定哪些导出被使用。ESModule 的 import 是静态声明,构建工具从一开始就能分析依赖图、剔除未使用代码。
现在项目中还用 AMD 和 CommonJS 吗?
CommonJS 依然大量存在——npm 上多数包仍是 CJS 格式。AMD 基本只在维护老项目时遇到。新项目统一用 ESModule,但 CJS 向 ESM 的迁移是个渐进过程,两种格式互操作(CJS 里 import ESM、ESM 里 require CJS)在一些边界场景仍有坑。
Node.js 原生支持 ESModule 了吗?
Node 12+ 已支持(.mjs 后缀或在 package.json 中设置 "type": "module"),但生态中大量 npm 包仍以 CJS 发布,双格式共存还会持续一段时间。