# 模块化
在 ES 6 模块规范出现之前,虽然浏览器原生不支持模块的行为,但迫切需要这样的行为。ES 同样不支持模块,因此希望使用模块模式的库或代码库必须基于 JavaScript 的语法和词法特性“伪造”出类似模块的行为。
# 1、理解模块
将代码拆分成独立的块,然后再把这些块连接起来可以通过模块模式来实现。这种模式背后的思想很简单:把逻辑分块,各自封装,相互独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。不同的实现和特性让这些基本的概念变得有点复杂,但这个基本的思想是所有 JavaScript 模块系统的基础。
# 1.1 函数封装
函数一个功能就是实现特定逻辑的一组语句打包,而且JavaScript的作用域就是基于函数的,所以把函数作为模块化的第一步是很自然的事情,在一个文件里面编写几个相关函数就是最开始的模块了。
function fn1() {
statement
}
function fn2() {
statement
}
2
3
4
5
6
7
这样在需要的地方调用相关的函数就可以了。
但是这样做有明显的缺陷:污染了全局变量,而且无法保证不与其他模块发生命名冲突,而且模块成员之间也没有什么关系。
# 1.2 对象封装
为了解决上面的问题,对象封装的写法应运而生,可以将有关系的函数封装到一个对象中。
const myModule = {
var1: 1,
var2: 2,
fn1: function () {
statement
},
fn2: function () {
statement
}
}
2
3
4
5
6
7
8
9
10
这样的话,我们在需要调用的时候引入对应的模块,然后 myModule.fn1();这样避免了全局污染,只要保证模块名称唯一,而且统一模块间的成员也有了关系。但是也有缺陷,因为我们可以随意修改模块内的变量,例如:myModule.var1 = 2;这样可能会产生意外的问题。
# 1.3 立即执行函数
立即执行函数(IIFE,Immediately Invoked Function Expression)将模块定义封装在匿名闭包中。模块定义是立即执行的,如下:
const myModule = (function() {
var var1 = 1;
var var2 = 2;
function fn1 () {
statement
}
function fn2 () {
statement
}
return {
fn1: fn1,
fn2: fn2
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2、ES6 之前的模块加载器
# 2.1 CommonJs
CommonJS 规范概述了同步声明依赖的模块定义。这个规范主要用于在服务器端实现模块化代码组织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。
一般认为,Node.js 的模块系统使用了 CommonJS 规范,实际上并不完全正确。Node.js 使用了轻微修改版本的 CommonJS,因为 Node.js 主要在服务器环境下使用,所以不需要考虑网络延迟问题。考虑到一致性,本节使用 Node.js 风格的模块定义语法。
Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用 module.exports 定义当前模块对外输出的接口(不推荐直接用exports),用 require 加载模块。
// 定义math.js
function add(a, b) {
return a + b;
}
module.exports = {
add: add
}
// index.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
2
3
4
5
6
7
8
9
10
11
在输入时先加载整个模块,生成一个对象,然后再从这个对象上面读取方法。
- CommonsJs 是运行时加载
- CommonJs 的拷贝是浅拷贝
# 2.2 AMD
CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存,而异步模块定义(AMD,Asynchronous Module Definition)的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的问题。AMD 的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并在依赖加载完成后立即执行依赖它们的模块。
AMD 模块实现的核心是用函数包装模块定义。这样可以防止声明全局变量,并允许加载器库控制何时加载模块。包装函数也便于模块代码的移植,因为包装函数内部的所有模块代码使用的都是原生 JavaScript 结构。包装模块的函数是全局 define 的参数,它是由 AMD 加载器库的实现定义的。包装模块的函数是全局 define 的参数,它是由 AMD 加载器库的实现定义的。
AMD 模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
// ID 为'moduleA'的模块定义。moduleA 依赖 moduleB,
// moduleB 会异步加载
define('moduleA', ['moduleB'], function(moduleB) {
return {
stuff: moduleB.doStuff();
};
});
2
3
4
5
6
7
# 2.3 UMD
为了统一 CommonJS 和 AMD 生态系统,通用模块定义(UMD,Universal Module Definition)规范应运而生。UMD 可用于创建这两个系统都可以使用的模块代码。本质上,UMD 定义的模块会在启动时检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE)中。
下面是只包含一个依赖的 UMD 模块定义的示例:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD。注册为匿名模块
define(['moduleB'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node。不支持严格 CommonJS
// 但可以在 Node 这样支持 module.exports 的
// 类 CommonJS 环境下使用
module.exports = factory(require(' moduleB '));
} else {
// 浏览器全局上下文(root 是 window)
root.returnExports = factory(root. moduleB);
}
}(this, function (moduleB) {
// 以某种方式使用 moduleB
// 将返回值作为模块的导出
// 这个例子返回了一个对象
// 但是模块也可以返回函数作为导出值
return {};
}));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
此模式有支持严格 CommonJS 和浏览器全局上下文的变体。不应该期望手写这个包装函数,它应该由构建工具自动生成。开发者只需专注于模块的内由容,而不必关心这些样板代码。
# 2.4 CMD
CMD 即Common Module Definition通用模块定义。
CMD 是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 3、ES6 模块
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
2
3
4
5
6
7
8
9
10
11
12
# 4、区别
# 4.1 AMD和CMD的区别
最明显的区别就是在模块定义时对依赖的处理不同。
- 1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块
- 2、CMD推崇就近依赖,只有在用到某个模块的时候再去require
这种区别各有优劣,只是语法上的差距。AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同。
# 4.2 ES6 模块与 CommonJS 模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(只是浅拷贝)。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
- 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
# 5、工作者模块
ES 6 模块与 Worker 实例完全兼容。在实例化时,可以给工作者传入一个指向模块文件的路径,与传入常规脚本文件一样。Worker 构造函数接收第二个参数,用于说明传入的是模块文件。
// 第二个参数默认为{ type: 'classic' }
const scriptWorker = new Worker('scriptWorker.js');
const moduleWorker = new Worker('moduleWorker.js', { type: 'module' });
2
3