模块可以理解为独立且通用的代码单元,是一个实现特定功能的文件。所谓模块化只要就是解决代码分隔、作用域隔离、模块之间的依赖管理等问题。
模块化的优点:
- 避免变量污染、命名冲突
- 可维护性;模块是独立的,测试、修改都可独立进行
- 重用代码;通过模块的导出、引入的方式,避免代码的拷贝
早期模块化方式
- 函数封装,缺点:污染全局变量,变量命名冲突,模块成员之间没有关系
- 对象封装,缺点:外部可随意修改内部成员变量
- 立即执行函数(闭包),缺点:代码量大,闭包有成本,影响性能
script
标签顺序引入 js 文件,缺点:污染全局变量,命名冲突,依赖关系不明显,不利于维护
模块化规范
常见的模块化规范有: CommonJS、AMD(Asynchronous Module Definition)、CMD(Common Module Definition)、ES6 Module
CommonJS
CommonJS 采用 同步加载模块的策略,只有加载完成后才会之下后续操作。
CommonJS 是服务端模块的规范,Node.js 就是采用该规范。
在 CommonJS 的规范中,每一个 js 文件就是一个独立的模块上下文(module context),在该上下文中默认创建的属性都是私有的。即在一个文件定义的变量,是私有的,对其他文件是不可见的。
require
和module
关键字,实现了模块导入、对外暴露1
2
3
4
5
6
7
8
9
10
11
12
13function aModule() {
let name = "jiang";
this.foo = function () {
console.log(">>> hello " + name);
};
this.bar = function () {};
}
module.exports = aModule; // 对外暴露
// 引入 模块 aModule
let a = require("./aModule");
let aa = new a();
aa.foo(); // >>> hello jiangrequire 中文件的查找策略
AMD 异步模块定义
AMD 优先照顾浏览器的模块加载场景,使用异步加载和回调的方式
使用
define
方法,来定义模块、引入模块。该方法需要三个参数:模块名称、依赖数组、回调函数RequireJS 遵循 AMD 规范
1
2
3
4
5
6
7
8define(["module1", "module2"], function (module1, module2) {
function foo() {
module1.foo();
}
return {
foo: foo,
};
});
ES6 Module
通过 import 引入模块,export 导出模块
每一个模块只加载一次,并缓存
一个模块就是一个单例
模块中的值属于 动态 只读 引用,不允许修该
1
2
3
4
5
6
7
8export let foo = {
name: "jiang",
age: "25",
}; // 导出
// 引入模块
import { foo } from "./foo.js";
console.log(foo.age);
ES6 模块与 CommonJS 模块的差异
CommonJS 模块输出的是一个值的拷贝,原始类型的值会被缓存,ES6 模块输出的是值的引用,且是动态的只读的引用,不会缓存,对它进行重新赋值会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// a.js
let counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter,
get getCounter() {
return counter;
},
incCounter,
};
// a1.js
let a = require("./a");
console.log(a); // { counter: 3, getCounter: [Getter], incCounter: [Function:incCounter] }
console.log(a.counter); // 3
a.incCounter();
console.log(a.counter); // 3
console.log(a.getCounter); // 4a.js
模块加载后,它的内部变化就影响不到counter
了,因为counter
是是一个原始类型的值,会被缓存。写成 get 函数,就能得到内部变动后的最新值了。1
2
3
4
5
6
7
8
9
10
11// b.js
export let counter = 3;
export function incCounter() {
counter++;
}
// b1.js
import { counter, incCounter } from "./b";
console.log(counter); // 3
incCounter();
console.log(counter); // 4ES6 模块,遇到
import
,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块去读值CommonJS 模块在运行时加载,ES6 模块在 编译时就输出接口,在静态解析阶段就会生成
处理循环加载不同,CommonJS 模块,脚本代码在
require
的时候,就会全部执行,一旦出现某个模块被”循环加载”,就只输出已执行的部分,还未执行的部分不会输出;ES6 模块是动态引用的,在真正取值的时候 需要 开发者自己去保证,能取到值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// c.js
exports.done = false;
var d = require("./d");
console.log("在c.js 中,d.done = %j", d.done);
exports.done = true;
console.log("c.j 执行完毕");
//d.js
exports.done = false;
var c = require("./c");
console.log("在 d.js 中, c.done = %j", c.done);
exports.done = true;
console.log("d.js 执行完毕");
//c_d.js
var c = require("./c");
var d = require("./d");
console.log("在 c_d.js 之中, c.done=%j, d.done=%j", c.done, d.done);1
2
3
4
5
6node.exe c_d.js
在 d.js 中, c.done = false
d.js 执行完毕
在c.js 中,d.done = true
c.j 执行完毕
在 c_d.js 之中, c.done=true, d.done=true