深入解析JavaScript模块化实现原理:从CommonJS到AMD/CMD
本文深入剖析JavaScript模块化发展的必要性及核心实现原理。从解决全局污染与代码组织问题出发,详细解析了CommonJS通过闭包和new Function执行模块字符串的机制,以及浏览器环境下因无法同步读取文件而衍生出的AMD与CMD异步加载规范。同时探讨了Node.js、React Native及Webpack中不同的模块载入策略,帮助开发者理解模块化规范背后的技术演进与底层逻辑。
JavaScript模块化背景与目标
JavaScript 早期设计时并未预见到复杂应用场景的需求,因此原生语言中缺乏模块系统。随着应用规模扩大,模块化成为必须解决的关键问题。为了深入理解其底层原理,有必要揭开模块化的实现机制。
一、模块化需要解决的问题
深入剖析任何技术体系前,明确其核心目标至关重要。模块化的核心诉求可以概括为以下一点:
在不产生全局污染的前提下,更高效地组织项目代码结构。
举例说明,假设现有如下代码逻辑:
function doSomething () {
const a = 10;
const b = 11;
const add = function (a + b) {
return a + b
}
add (a + b)
}
在实际业务场景中,doSomething 函数可能包含大量逻辑,而 add 函数具备复用价值。我们希望将 add 函数独立至单独文件,实现如下结构:
// doSomething.js 文件
const add = require('add.js');
const a = 10;
const b = 11;
add(a+ b);
// add.js 文件
function add (a, b) {
return a + b;
}
module.exports = add;
此举旨在优化代码组织。观察代码中的 require 和 module.exports,从全局视角看,这对应 CommonJS 规范中的导入导出关键字(后续章节将详述规范)。抛开规范形式,这揭示了模块化需要解决的核心问题:既要复用模块,又要避免引入时造成全局污染。
模块运行机制:闭包与Function构造器
二、引入的模块如何运行
在上述示例中,代码已拆分为两个模块文件。在不污染全局作用域的前提下,如何实现 require 以保障代码正常运行?
暂不考虑模块文件的加载过程,假设 require 已能读取模块文件中的代码字符串,其实现逻辑大致如下:
function require (path) {
// lode 方法读取 path 对应的文件模块的代码字符串
// let code = load(path);
// 不考虑 load 的过程,直接获得模块 add 代码字符串
let code = 'function add(a, b) {return a+b}; module.exports = add';
// 封装成闭包
code = `(function(module) {$[code]})(context)`
// 相当于 exports,用于导出对象
let context = {};
// 运行代码,使得结果影响到 context
const run = new Function('context', code);
run(context, code);
//返回导出的结果
return context.exports;
}
该实现包含几个关键点:
1)为避免全局污染,需将代码字符串封装为闭包形式。关键字 module.exports 用于导出,module 是与外部交互的唯一载体,需作为闭包匿名函数的参数,并与引用方传入的上下文 context 关联。
2)使用 new Function 执行代码字符串。许多开发者对 new Function 较为陌生,因为通常定义函数无需如此操作。Function 类可直接创建函数,语法如下:
var function_name = new function(arg1, arg2, ..., argN, function_body)
在上述语法中,每个 arg 均为参数,最后一个参数为函数主体(即要执行的代码)。这些参数必须是字符串。这意味着可以利用它执行字符串代码,类似于 eval,但相比 eval,它还支持通过参数形式传入字符串代码中某些变量的值。
3)若曾疑惑为何规范中导出关键字仅为 exports,而实际使用中需使用 module.exports(Node.js 开发者应不陌生),此段代码可给出答案。若仅用 exports 接收 context,重新赋值 exports 不会影响 context(因参数为地址传递)。可将代码修改为如下形式进行测试:

演示结果
不同环境下的模块载入策略
三、代码载入方式
解决代码运行问题后,还需解决模块文件代码的载入问题。根据上述实例,目标是将模块文件代码以字符串形式载入。
在 Node 环境中,所有模块文件位于本地,只需从本地磁盘读取模块文件并载入字符串代码,再执行上述流程即可。事实证明,Node 中非内建、非核心、非 C++ 模块的载入执行方式大体如此(尽管未使用 new Function,但采用了类似方法)。
在 RN/Weex 容器中,若要载入远程 bundle.js,可通过 Native 能力请求远程 JS 文件,再读取为字符串代码载入(按此逻辑,Node 读取远程 JS 模块亦可行,尽管多数情况下无需如此)。
在浏览器环境中,所有 JS 模块均需远程读取。受限于浏览器能力,无法通过 ajax 以文件流形式将远程 JS 文件直接读取为字符串代码。前提条件无法达成,上述运行策略便行不通,只能另辟蹊径。
这解释了为何在 CommonJS 规范存在的情况下,仍会出现 AMD/CMD 规范。
那么浏览器端如何实现呢?在浏览器中通过 JS 控制动态载入远程 JS 模块文件,需动态插入一个 <script> 节点:
// 摘抄自 require.js 的一段代码
var node = config.xhtml ?
document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
document.createElement('script');
node.type = config.scriptType || 'text/javascript';
node.charset = 'utf-8';
node.async = true;
node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremodule', moduleName);
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
需注意,设置 <script> 标签的 src 后,代码一旦下载完成便会立即执行,无法再封装为闭包。因此,文件模块需在定义之初做文章,即我们熟知的 AMD/CMD 规范中的 define,开篇的 add.js 需重新改写为:
// add.js 文件
define ('add',function () {
function add (a, b) {
return a + b;
}
return add;
})
对于 define 的实现,核心在于将 callback 的执行结果注册到 context 的一个模块数组中:
context.modules = {}
function define(name, callback) {
context.modules[name] = callback && callback()
}
于是,require 即可从 context.modules 中根据模块名载入模块。这是否激发了你自行编写一个 “requirejs” 的冲动?
具体的 AMD 实现显然更为复杂,还需控制模块载入时序、模块依赖等,但理解了这一核心逻辑,精读 require.js 源码便不再困难。
Webpack中的模块化实现
四、Webpack 中的模块化
Webpack 亦可配置异步模块。当配置为异步模块时,在浏览器环境下同样基于动态插入 <script> 的方式载入远程模块。在大多数情况下,模块的载入方式类似于 Node 的本地磁盘同步载入方式。
勿忘,Webpack 除具备模块化能力外,还是辅助完善开发工作流的工具。也就是说,Webpack 的模块化是在开发阶段完成的。使用 Webpack 构筑的工作环境,在开发阶段虽是独立的模块文件,但在运行时,却是一个合并好的文件。
因此,Webpack 是一种在非运行时的模块化方案(基于 CommonJs)。只有在配置了异步模块时,对异步模块的加载才是运行时的(基于 AMD)。
CommonJS、AMD与CMD规范解析
五、模块化规范
通用问题在解决过程中总会形成规范。上文已多次提及 CommonJs、AMD、CMD,有必要花篇幅讲解这些规范。
Js 的模块化规范萌发于将 Js 扩展到后端的构想。要使 Js 具备类似 Python、Ruby 和 Java 开发大型应用的基础能力,模块化规范必不可少。CommonJS 规范的提出为 Js 制定了美好愿景,希望 Js 能在任何地方运行,包括但不限于:
- 服务器端 Js 应用
- 命令行工具
- 桌面应用
- 混合应用
CommonJS 对模块的定义并不复杂,主要分为模块引用、模块定义和模块标识:
- 模块引用:使用 require 方法来引入一个模块
- 模块定义:使用 exports 导出模块对象
- 模块标识:给 require 方法传入的参数,小驼峰命名的字符串、相对路径或者绝对路径

模块示意
CommonJs 规范在 Node 中大放异彩并相互促进,但在浏览器端,鉴于网络原因,同步方式加载模块显然不太实用。经过一段争执后,AMD 规范最终在前端场景中胜出(全称 Asynchronous Module Definition,即“异步模块定义”)。
什么是 AMD,为什么需要 AMD?在前述模块化实现的推演过程中,你应该能够找到答案。
此外还有国内玉伯提出的 CMD 规范。AMD 和 CMD 的差异主要是,前者需要在定义之初声明所有的依赖,后者可以在任意时机动态引入模块。CMD 更接近于 CommonJS。
两种规范都需要从远程网络中载入模块,不同之处在于,前者是预加载,后者是延迟加载。
五、总结
如果有心,可以参照本文的推演,来实现一个 “yourRequireJs”,没有什么比重复造轮子更能让知识沉淀。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
