# JavaScript教程 - 20 模块化
模块化就是将代码按照功能分割为独立、可复用的模块,这样利于代码的组织和管理以及代码复用。还可以解决了全局变量污染等问题。
# 20.1 立即执行函数
在以前的 JavaScript 中,使用立即执行函数表达式(IIFE),通过闭包的方式来模拟模块化,但缺乏标准化的依赖管理。
举个栗子:
我们定义一个 moduleA.js
,内容如下:
// 模块A
const moduleA = (function() {
let privateVar = 'A的私有变量';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
2
3
4
5
6
7
8
9
10
11
12
- 在上面的代码中创建了一个立即执行函数,并将函数的返回结果赋值给 moduleA,这个函数其实是构成了一个闭包,函数内的变量外部是无法访问的,避免被其他代码修改,造成污染。
在使用的时候,引入上面的 js 并使用:
<script src="moduleA.js"></script>
<script>
moduleA.publicMethod(); // 输出: A的私有变量
</script>
2
3
4
- 但是 IIFE 这种方式无法声明依赖关系,只能依赖 script 的加载顺序,如果模块 A 依赖模块 B,必须先引入 B,否则就报错。所以说 IIFE 只是语法技巧,并不是真正意义上的模块机制,它不能自动管理依赖、导出、导入。
# 20.2 现代模块化方式
ECMAScript 2015(ES6)引入的原生模块系统,可以使用 export
导出模块成员,使用 import
导入模块成员,并且支持静态解析(编译时确定依赖关系)。
# 1 基础导出/导入
例如,定义 math.js
,使用 export
分别导出一个常量和一个函数:
// math.js
export const PI = 3.14; // 导出一个常量
// 加法
export function add(a, b) { // 导出一个函数
return a + b;
}
2
3
4
5
6
7
在使用的时候,可以通过 import
导入并使用:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>For技术栈</title>
<script type="module">
// main.js
import { PI, add } from "./script/math.js";
console.log(PI); // 3.14
console.log(add(1, 4)); // 5
</script>
</head>
<body>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 需要注意,
<script>
标签需要添加type="module"
属性,才可以进行引入和使用。 import { PI, add }
对应math.js
中定义的常量和方法,顺序无所谓;export
把变量或函数导出,必须使用导出的名字来导入。所以这种方式也叫命名导出。
有时候我们不会在 html 文件中编写 js 逻辑,而是将所有的逻辑都写在 js 文件中,然后在 html 中导入。如果编写的 js 文件中使用了 import
/ export
,那么引入该 js 文件,就必须在 <script>
标签上写 type="module"
,否则浏览器会报错。
例如上面单纯的导入 math.js
,需要这样写:
<script type="module" src="./script/math.js"></script>
在一个 JavaScript 模块文件(也就是一个 js 文件,比如 math.js
)中定义的变量、函数、类等, 默认只在该模块内部可见,除非被导出。所以在同时引入这些 js 的时候也是不会冲突的。
举个栗子:
// math.js
const token = 'abc123'; // 模块私有,只能在模块内使用
let counter = 0; // 模块私有,只能在模块内使用
var legacySupport = true; // 不推荐使用 var
export const name = 'MyModule'; // 导出变量
2
3
4
5
6
前提是务必使用 <script type="module" ...>
方式来引入模块!
# 2 默认导出/导入
一个模块,也就是一个 js 文件,还可以有一个默认导出,一般用来导出模块中最核心、最主要的内容。
举个栗子:
定义一个 greet.js
// 使用 export default 默认导出 greet 函数
export default function greet(name) {
console.log(`Hello, ${name}`);
}
2
3
4
等价于:
function greet(name) {
console.log(`Hello, ${name}`);
}
export default greet;
2
3
4
- 也可以默认导出变量、对象、类等。
- 需要注意:一个模块只能有一个默认导出。
在导入默认导出的时候,可以自定义名称。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>For技术栈</title>
<script type="module">
import greet from './script/greet.js'; // 导入默认导出
greet('For技术栈'); // Hello, For技术栈
</script>
</head>
<body>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 上面导入的时候,指定了默认导出的名称为
greet
,其实可以是自定义的。 - 导入默认导出是,不要添加
{}
。
例如可以这样写:
<script type="module">
import sayHi from './script/greet.js';
sayHi('For技术栈'); // Hello, For技术栈
</script>
2
3
4
# 3 命名导出和默认导出混用
命名导出和默认导出是可以混用的,没有任何问题。
举个栗子:
定义 math.js
如下:
// math.js
export const PI = 3.14; // 导出一个常量
// 加法
export function add(a, b) { // 导出一个函数
return a + b;
}
// 减法
export default function subtract(a, b) { // 默认导出
return a - b;
}
2
3
4
5
6
7
8
9
10
11
12
那么可以这样导入:
<script type="module">
// main.js
import subtract, { PI, add } from "./script/math.js";
console.log(PI); // 3.14
console.log(add(1, 4)); // 5
console.log(subtract(4, 1)); // 3
</script>
2
3
4
5
6
7
需要注意,如果导入默认导出和命名导出,默认导出需要写在前面,就像上面那样。
下面这样写是会报错的:
import { PI, add }, subtract from "./script/math.js"; // ❌ 错误
当然,你也可以按需导入,使用什么就导入什么。
# 4 名称冲突
导入默认导出,名称可以自定义。但是如果导入多个模块的命名导出,名称存在冲突,如何解决?
可以通过别名的方式导入。
举个栗子:
loggerA.js
// loggerA.js
export function log(message) {
console.log('Logger A:', message);
}
2
3
4
loggerB.js
// loggerB.js
export function log(message) {
console.log('Logger B:', message);
}
2
3
4
上面两个模块同时存在同名的函数,同时导入上面的两个模块的 log
函数,导入的时候可以使用别名。
举个栗子:
<script type="module">
// 使用 as 重命名(alias)
import { log as logA } from "./script/loggerA.js";
import { log as logB } from "./script/loggerB.js";
logA("Hello"); // 输出:Logger A: Hello
logB("For技术栈"); // 输出:Logger B: For技术栈
</script>
2
3
4
5
6
7
8
- 上面在导入命名导出的时候,给两个同名的函数分别使用
as
关键字起了别名,这样使用别名调用就可以了。
# 5 全部导入
在 JavaScript 的模块系统中,还可以使用 import * as ...
的语法将一个模块的所有导出内容一次性全部导入,这种方式叫做:命名空间导入(namespace import)。
语法格式:
import * as 模块别名 from '模块路径';
例如有:math.js
// math.js
export const PI = 3.14; // 导出一个常量
// 加法
export function add(a, b) { // 导出一个函数
return a + b;
}
// 减法
export default function subtract(a, b) { // 默认导出
return a - b;
}
2
3
4
5
6
7
8
9
10
11
12
然后在 HTML 页面中这样导入:
<script type="module">
import * as math from "./script/math.js";
console.log(math.PI); // 3.14
console.log(math.add(1, 4)); // 5
// console.log(subtract(4, 1)); // ❌报错
</script>
2
3
4
5
6
7
- 全部导入可以将所有的命名导出全部导入到一个对象,即
math
是一个对象(类似命名空间),所有导出的函数、常量都成为它的属性;如果有很多导出,但你不想一个个列出来,import * as ...
非常方便。 - 而且这种方式也可以解决命名导出可能存在的名称冲突问题;
- 但是需要注意,
import * as math from './script/math.js'
不会包含默认导出。
如果你既要导入全部命名导出,又要拿到默认导出,可以这样:
<script type="module">
import subtract, * as math from './script/math.js'; // 导入默认导出和命名空间导入
console.log(math.PI); // 3.14
console.log(math.add(1, 4)); // 5
console.log(subtract(4, 1)); // 3
</script>
2
3
4
5
6
7
# 6 多个模块相互引入
在 JavaScript 的模块系统中,各个模块可以互相导入调用,这样每个模块可以专注于自己的功能,同时可以使用其他模块的能力,实现代码的高内聚、低耦合。
举个栗子:模块 A 调用模块 B,模块 B 又调用模块 C。
文件结构:
project/
├── index.html
├── a.js // 模块 A(入口)
├── b.js // 模块 B
└── c.js // 模块 C
2
3
4
5
c.js:最底层模块
export function say(message) {
console.log('C says:', message);
}
2
3
b.js:中间层模块,依赖 C
import { say } from './c.js'; // 导入C模块
export function greet(name) {
say(`Hello, ${name}`);
}
2
3
4
5
a.js:顶层模块,依赖 B
import { greet } from './b.js'; // 导入B模块
greet('For技术栈'); // 最终输出:C says: Hello, For技术栈
2
3
- 上面定义了三个模块,A、B、C,模块 A 调用模块 B,模块 B 又调用模块 C。
在 index.html 引入模块A 即可。
index.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>For技术栈</title>
<script type="module" src="./a.js"></script>
</head>
<body></body>
</html>
2
3
4
5
6
7
8
9
10
← 19-异步编程 21-浏览器的本地存储 →