本文深入解析 JavaScript 模块化发展历程,对比 CommonJS 与 ES6 Module 两大核心规范。详细阐述 require、import、export 的语法差异、执行机制及适用场景,剖析 AMD/CMD 规范背景,并解释 module.exports 与 exports 的关系,帮助开发者掌握现代前端模块化管理。
JavaScript 模块化概述
前言
本文主要介绍了 JavaScript 中模块化的核心概念,重点解析 require、import 和 export 的使用方法与区别,旨在为开发者提供清晰的参考与学习路径。
为什么需要模块化概念
在理想的开发模式中,开发者应专注于核心业务逻辑的实现,其他通用功能应通过加载已有的第三方模块来完成。
然而,JavaScript 早期并非一门支持模块化的语言。在 ES6 标准发布之前,JavaScript 不支持“类”(class)的概念,因此也缺乏原生的“模块”(module)机制。
CommonJS 规范与早期探索
CommonJS 时代:模块化的探索
为了在现有的运行环境中实现模块化效果,JavaScript 社区进行了大量探索与实践。
原始写法:全局变量污染
模块本质上是实现特定功能的一组方法。只要将不同的函数(以及记录状态的变量)简单地组合在一起,即可构成一个模块。
function m1(){
//...
}
function m2(){
//...
}
上述代码中的 m1() 和 m2() 两个函数共同组成了一个模块。使用时直接调用这些函数即可。
这种原始写法存在明显缺陷:它会“污染”全局变量,无法避免与其他模块发生变量名冲突,且模块成员之间的依赖关系不直观。
对象写法:封装模块成员
为解决上述问题,可以将模块改写为对象形式,将所有模块成员放置在该对象内部。
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
上述代码中的 m1() 和 m2() 均被封装在 module1 对象中。使用时,通过调用该对象的属性来访问模块功能。
module1.m1();
这种写法会暴露所有模块成员,导致内部状态可被外部随意修改。例如,外部代码可以直接修改内部计数器的值。
module._count = 1;
立即执行函数写法:保护私有成员
使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),可以实现不暴露私有成员的目的。
var module = (function() {
var _count = 0;
var m1 = function() {
alert(_count)
}
var m2 = function() {
alert(_count + 1)
}
return {
m1: m1,
m2: m2
}
})()
采用上述写法后,外部代码无法直接读取内部的 _count 变量,从而实现了私有变量的保护。
console.info(module._count); //undefined
IIFE 模式构成了 JavaScript 早期模块的基本写法。
主流模块规范:CommonJS 与 AMD
在 ES6 标准提出之前,官方并未制定统一的模块规范。从社区推广和框架普及程度来看,目前通行的 JavaScript 模块规范主要有两种:CommonJS 和 AMD。
CommonJS 规范详解

2009 年,美国程序员 Ryan Dahl 创建了 Node.js 项目,将 JavaScript 语言应用于服务器端编程。
这标志着“JavaScript 模块化编程”的正式诞生。前端开发复杂度有限,不使用模块尚可接受;但在服务器端,必须通过模块与操作系统及其他应用程序交互,否则无法进行大规模编程。
Node 编程的核心思想之一是模块化,正是这一思想使得 JavaScript 的大规模工程成为可能。模块化编程在 JavaScript 领域流行,也基于此。随后,浏览器端出现了 RequireJS 和 SeaJS 等工具包。可以说,在对应规范下,require 统治了 ES6 之前的所有模块化编程。即使在 ES6 Module 完全普及之前,这一局面依然保持。
在 CommonJS 规范中,暴露模块使用 module.exports 和 exports。许多人疑惑为何暴露对象会有两种方式,下文将介绍其区别。
在 CommonJS 中,有一个全局性方法 require() ,用于加载模块。假定有一个数学模块 math.js,加载方式如下所示。
var math = require('math');
加载完成后,即可调用模块提供的方法。
var math = require('math');
math.add(2,3); // 5
正是由于 CommonJS 采用的 require 方式推动了模块化发展,后续的 AMD 和 CMD 规范也采用了类似的 require 方式来引用模块。
AMD 与 CMD 规范解析
AMD 规范:异步模块定义

服务器端模块出现后,开发者自然希望实现客户端模块,并力求两者兼容,即一个模块无需修改即可在服务器和浏览器中运行。
然而,由于一个重大局限,使得 CommonJS 规范不适用于浏览器环境。以上一节代码为例,如果在浏览器中运行,会存在严重问题。
var math = require('math');
math.add(2, 3);
代码第二行的 math.add(2, 3) 必须在第一行的 require(‘math') 之后运行,因此必须等待 math.js 加载完成。如果加载时间过长,整个应用将停滞等待。
这对服务器端并非问题,因为所有模块均存储在本地硬盘,可以同步加载完成,等待时间仅为硬盘读取时间。但对于浏览器,模块位于服务器端,等待时间取决于网速,可能导致浏览器处于“假死”状态。
因此,浏览器端的模块不能采用“同步加载”(synchronous),只能采用“异步加载”(asynchronous)。这就是 AMD 规范诞生的背景。
AMD 是“Asynchronous Module Definition”的缩写,意为“异步模块定义”。它采用异步方式加载模块,模块加载不影响后续语句的运行。所有依赖该模块的语句均定义在一个回调函数中,待加载完成后,该回调函数才会执行。
模块必须采用特定的 define() 函数来定义。
define(id?, dependencies?, factory)
- id:字符串,模块名称(可选)
- dependencies:要载入的依赖模块(可选),使用相对路径,注意必须是数组格式。
- factory:工厂方法,返回一个模块函数。
如果一个模块不依赖其他模块,可以直接在 define() 函数中定义。
// math.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
如果该模块依赖其他模块,define() 函数的第一个参数必须是数组,指明该模块的依赖项。
define(['Lib'], function(Lib){
function foo(){
Lib.doSomething();
}
return {
foo : foo
};
});
当 require() 函数加载上述模块时,会先加载 Lib.js 文件。
AMD 也采用 require() 语句加载模块,但与 CommonJS 不同,它要求两个参数。
require([module], callback);
第一个参数 [module] 是一个数组,包含要加载的模块;第二个参数 callback 是加载成功后的回调函数。将前述代码改写为 AMD 形式如下所示。
require(['math'], function (math) {
math.add(2, 3);
});
math.add() 与 math 模块的加载不同步,浏览器不会发生假死。因此,AMD 更适合浏览器环境。
目前,主要有两个 JavaScript 库实现了 AMD 规范:Require.js 和 Curl.js。
CMD 规范:依赖就近

CMD (Common Module Definition) 是 SeaJS 推崇的规范。CMD 强调依赖就近,使用时再 require。其写法如下所示。
define(function(require, exports, module) {
var clock = require('clock');
clock.start();
});
CMD 与 AMD 一样,采用特定的 define() 函数来定义,并使用 require 方式引用模块。
define(id?, dependencies?, factory)
- id:字符串,模块名称(可选)
- dependencies:要载入的依赖模块(可选),使用相对路径,注意必须是数组格式。
- factory:工厂方法,返回一个模块函数。
define('hello', ['jquery'], function(require, exports, module) {
// 模块代码
});
如果一个模块不依赖其他模块,可以直接在 define() 函数中定义。
define(function(require, exports, module) {
// 模块代码
});
注意:带有 id 和 dependencies 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。
CMD 与 AMD 的区别
AMD 和 CMD 的最大区别在于对依赖模块的执行时机处理不同,而非加载时机或方式不同,二者均为异步加载模块。
AMD 依赖前置,JavaScript 可以方便地知道依赖模块是谁,并立即加载;
而 CMD 依赖就近,需要使用字符串解析模块后才能知道依赖了哪些模块。这也是许多人诟病 CMD 的一点,即牺牲性能以换取开发的便利性。实际上,解析模块所用的时间极短,可以忽略不计。
ES6 Module 标准详解
现阶段的标准:ES6 Module

ES6 标准发布后,module 成为标准。标准规定使用 export 指令导出接口,以 import 引入模块。但在传统的 Node.js 模块中,我们依然采用 CommonJS 规范,使用 require 引入模块,使用 module.exports 导出接口。
export 导出模块详解
export 语法声明用于导出函数、对象、指定文件(或模块)的原始值。
注意:在 Node.js 中使用的是 exports,请勿与 ES6 的 export 混淆。
export 有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出)。命名式导出每个模块可以有多个,而默认导出每个模块仅有一个。
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
- name1… nameN:导出的“标识符”。导出后,可通过该“标识符”在另一个模块中使用 import 引用。
- default:设置模块的默认导出。设置后,import 不通过“标识符”而直接引用默认导入。
- *:继承模块并导出继承模块的所有方法和属性。
- as:重命名导出“标识符”。
- from:从已存在的模块、脚本文件等导出。
命名式导出示例
模块可以通过 export 前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称为命名式导出。
export { myFunction }; // 导出一个已定义的函数
export const foo = Math.sqrt(2); // 导出一个常量
我们可以使用 * 和 from 关键字来实现模块的继承。
export * from 'article';
模块导出时,可以指定模块的导出成员。导出成员可视为类中的公有对象,而非导出成员可视为类中的私有对象。
var name = 'IT笔录';
var domain = 'http://itbilu.com';
export {name, domain}; // 相当于导出
{name:name,domain:domain}
模块导出时,我们可以使用 as 关键字对导出成员进行重命名。
var name = 'IT笔录';
var domain = 'http://itbilu.com';
export {name as siteName, domain};
注意:以下语法存在严重错误情况。
// 错误演示
export 1; // 绝对不可以
var a = 100;
export a;
export 在导出接口时,必须与模块内部的变量具有一一对应的关系。直接导出数字 1 没有任何意义,也不可能在 import 时有一个变量与之对应。
export a 虽然看似成立,但 a 的值是一个数字,根本无法完成解构,因此必须写成 export {a} 的形式。即使 a 被赋值为一个函数,也不允许直接导出变量名。此外,大部分编码风格建议,模块中最好在末尾用一个 export 导出所有接口,例如:
export {fun as default,a,b,c};
默认导出详解
默认导出也被称为定义式导出。命名式导出可以导出多个值,但在 import 引用时,需使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,该输出可以是函数、类或其他类型的值,这样在模块 import 导入时也会更容易引用。
export default function() {}; // 可以导出一个函数
export default class(){}; // 也可以出一个类
命名式导出与默认导出的对比
默认导出可理解为另一种形式的命名导出,默认导出可视为使用了 default 名称的命名导出。
以下两种导出方式是等价的。
const D = 123;
export default D;
export { D as default };
export 使用示例
使用名称导出一个模块时:
// "my-module.js" 模块
export function cube(x) {
return x * x * x;
}
const foo = Math.PI + Math.SQRT2;
export { foo };
在另一个模块(脚本文件)中,我们可以按以下方式引用。
import { cube, foo } from 'my-module';
console.log(cube(3)); // 27
console.log(foo); // 4.555806215962888
使用默认导出一个模块时:
// "my-module.js"模块
export default function (x) {
return x * x * x;
}
在另一个模块(脚本文件)中,我们可以按以下方式引用,相对名称导出来说使用更为简单。
// 引用 "my-module.js"模块
import cube from 'my-module';
console.log(cube(3)); // 27
import 引入模块详解
import 语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import 模块导入与 export 模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。
import 的语法与 require 不同,且 import 必须放在文件的最开始,且前面不允许有其他逻辑代码,这与其他所有编程语言的风格一致。
import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";
- name:从将要导入模块中收到的导出值的名称。
- member, memberN:从导出模块,导入指定名称的多个成员。
- defaultMember:从导出模块,导入默认导出成员。
- alias, aliasN:别名,对指定导入成员进行的重命名。
- module-name:要导入的模块,是一个文件名。
- as:重命名导入成员名称(“标识符”)。
- from:从已存在的模块、脚本文件等导入。
命名式导入示例
我们可以通过指定名称,将这些成员插入到当前作用域中。导出时,可以导入单个成员或多个成员。
注意:花括号里面的变量与 export 后面的变量一一对应。
import {myMember} from "my-module";
import {foo, bar} from "my-module";
通过 * 符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块('my-module.js')所有的导出绑定内容,插入到当前模块('myModule')的作用域中。
import * as myModule from "my-module";
导入模块对象时,也可以使用 as 对导入成员重命名,以方便在当前模块内使用。
import {reallyReallyLongModuleMemberName as shortName} from "my-module";
导入多个成员时,同样可以使用别名。
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";
导入一个模块,但不进行任何绑定。
import "my-module";
默认导入示例
在模块导出时,可能会存在默认导出。同样,在导入时可以使用 import 指令导入这些默认值。
直接导入默认值:
import myDefault from "my-module";
也可以在命名空间导入和名称导入中,同时使用默认导入。
import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用
或
import myDefault, {foo, bar} from "my-module"; // 指定成员导入
import 使用示例
// --file.js--
function getJSON(url, callback) {
let xhr = new XMLHttpRequest();
xhr.onload = function () {
callback(this.responseText)
};
xhr.open("GET", url, true);
xhr.send();
}
export function getUsefulContents(url, callback) {
getJSON(url, data => callback(JSON.parse(data)));
}
// --main.js--
import { getUsefulContents } from "file";
getUsefulContents("http://itbilu.com", data => {
doSomethingUseful(data);
});
default 关键字用法
// d.js
export default function() {}
// 等效于:
function a() {};
export {a as default};
在 import 的时候,可以这样用:
import a from './d';
// 等效于,或者说就是下面这种写法的简写,是同一个意思
import {default as a} from './d';
这个语法糖的好处是,import 的时候可以省去花括号 {}。
简单来说,如果 import 时发现某个变量没有花括号括起来(没有 * 号),那么应在脑海中将其还原为有花括号的 as 语法。
因此,以下这种写法也应能理解。
import $,{each,map} from 'jquery';
import 后面第一个 $ 是 {defalut as $} 的替代写法。
as 关键字用法
as 简单来说就是取一个别名,export 中可以用,import 中其实也可以用。
// a.js
var a = function() {};
export {a as fun};
// b.js
import {fun as a} from './a';
a();
上述代码中,export 时对外提供的接口是 fun,它是 a.js 内部 a 这个函数的别名。但在模块外面,认不到 a,只能认到 fun。
import 中的 as 很简单,即在使用模块里面的方法时,给该方法取一个别名,以便在当前文件中使用。之所以如此,是因为有时不同的两个模块可能通过相同的接口,例如有一个 c.js 也通过了 fun 这个接口。
// c.js
export function fun() {};
如果在 b.js 中同时使用 a 和 c 这两个模块,就必须想办法解决接口重名的问题,as 关键字即可解决此问题。
module.exports 与 exports 区别
CommonJS 中 module.exports 与 exports 的区别
Module.exports
The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Note that assigning the desired object to exports will simply rebind the local exports variable, which is probably not what you want to do.
译文:module.exports 对象是由模块系统创建的。有时这是难以接受的;许多人希望他们的模块成为某个类的实例。为了实现这个,需要将期望导出的对象赋值给 module.exports。注意,将期望的对象赋值给 exports 会简单地重新绑定到本地 exports 变量上,这可能不是你想要的。
Module.exports
The exports variable is available within a module's file-level scope, and is assigned the value of module.exports before the module is evaluated. It allows a shortcut, so that module.exports.f = … can be written more succinctly as exports.f = …. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:
译文:exports 变量是在模块的文件级别作用域内有效的,它在模块被执行前被赋于 module.exports 的值。它有一个快捷方式,以便 module.exports.f = … 可以被更简洁地写成 exports.f = … 。注意,就像任何变量,如果一个新的值被赋值给 exports,它就不再绑定到 module.exports(其实是 exports. 属性会自动挂载到没有命名冲突的 module.exports. 属性)。
从 API 文档可以看出,从 require 导入方式去理解,关键有两个变量(全局变量 module.exports,局部变量 exports)、一个返回值(module.exports)。
function require(...) {
var module = { exports: {} };
((module, exports) => {
// 你的被引入代码 Start
// var exports = module.exports = {}; (默认都有的)
function some_func() {};
exports = some_func;
// 此时,exports不再挂载到module.exports,
// export将导出{}默认对象
module.exports = some_func;
// 此时,这个模块将导出some_func对象,覆盖exports上的some_func
// 你的被引入代码 End
})(module, module.exports);
// 不管是exports还是module.exports,最后返回的还是module.exports
return module.exports;
}
demo.js 示例代码。
console.log(exports); // {}
console.log(module.exports); // {}
console.log(exports === module.exports); // true
console.log(exports == module.exports); // true
console.log(module);
/**
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/larben/Desktop/demo.js',
loaded: false,
children: [],
paths:
[ '/Users/larben/Desktop/node_modules',
'/Users/larben/node_modules',
'/Users/node_modules',
'/node_modules' ] }
*/
注意
每个 js 文件一创建,都有一个 var exports = module.exports = {},使 exports 和 module.exports 都指向一个空对象。
module.exports 和 exports 所指向的内存地址相同。
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对本站的支持。