# JavaScript教程 - 20 模块化

模块化就是将代码按照功能分割为独立、可复用的模块,这样利于代码的组织和管理以及代码复用。还可以解决了全局变量污染等问题。

# 20.1 立即执行函数

在以前的 JavaScript 中,使用立即执行函数表达式(IIFE),通过闭包的方式来模拟模块化,但缺乏标准化的依赖管理。

举个栗子:

我们定义一个 moduleA.js,内容如下:

// 模块A
const moduleA = (function() {
  let privateVar = 'A的私有变量';
  function privateMethod() {
    console.log(privateVar);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  };
})();
1
2
3
4
5
6
7
8
9
10
11
12
  • 在上面的代码中创建了一个立即执行函数,并将函数的返回结果赋值给 moduleA,这个函数其实是构成了一个闭包,函数内的变量外部是无法访问的,避免被其他代码修改,造成污染。

在使用的时候,引入上面的 js 并使用:

<script src="moduleA.js"></script>
<script>
  moduleA.publicMethod(); // 输出: A的私有变量
</script>
1
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; 
}
1
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>
1
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>
1

在一个 JavaScript 模块文件(也就是一个 js 文件,比如 math.js)中定义的变量、函数、类等, 默认只在该模块内部可见除非被导出。所以在同时引入这些 js 的时候也是不会冲突的。

举个栗子:

// math.js
const token = 'abc123';        // 模块私有,只能在模块内使用
let counter = 0;               // 模块私有,只能在模块内使用
var legacySupport = true;      // 不推荐使用 var

export const name = 'MyModule'; // 导出变量
1
2
3
4
5
6

前提是务必使用 <script type="module" ...> 方式来引入模块

# 2 默认导出/导入

一个模块,也就是一个 js 文件,还可以有一个默认导出,一般用来导出模块中最核心、最主要的内容。

举个栗子:

定义一个 greet.js

// 使用 export default 默认导出 greet 函数
export default function greet(name) {
  console.log(`Hello, ${name}`);
}
1
2
3
4

等价于:

function greet(name) {
  console.log(`Hello, ${name}`);
}
export default greet;
1
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>
1
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>
1
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;
}
1
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>
1
2
3
4
5
6
7

需要注意,如果导入默认导出和命名导出,默认导出需要写在前面,就像上面那样。

下面这样写是会报错的:

import { PI, add }, subtract from "./script/math.js";   // ❌ 错误
1

当然,你也可以按需导入,使用什么就导入什么。

# 4 名称冲突

导入默认导出,名称可以自定义。但是如果导入多个模块的命名导出,名称存在冲突,如何解决?

可以通过别名的方式导入。

举个栗子:

loggerA.js

// loggerA.js
export function log(message) {
  console.log('Logger A:', message);
}
1
2
3
4

loggerB.js

// loggerB.js
export function log(message) {
  console.log('Logger B:', message);
}
1
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>
1
2
3
4
5
6
7
8
  • 上面在导入命名导出的时候,给两个同名的函数分别使用 as 关键字起了别名,这样使用别名调用就可以了。

# 5 全部导入

在 JavaScript 的模块系统中,还可以使用 import * as ... 的语法将一个模块的所有导出内容一次性全部导入,这种方式叫做:命名空间导入(namespace import)。

语法格式:

import * as 模块别名 from '模块路径';
1

例如有: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;
}
1
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>
1
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>
1
2
3
4
5
6
7

# 6 多个模块相互引入

在 JavaScript 的模块系统中,各个模块可以互相导入调用,这样每个模块可以专注于自己的功能,同时可以使用其他模块的能力,实现代码的高内聚、低耦合。

举个栗子:模块 A 调用模块 B,模块 B 又调用模块 C。

文件结构:

project/
├── index.html
├── a.js        // 模块 A(入口)
├── b.js        // 模块 B
└── c.js        // 模块 C
1
2
3
4
5

c.js:最底层模块

export function say(message) {
  console.log('C says:', message);
}
1
2
3

b.js:中间层模块,依赖 C

import { say } from './c.js';   // 导入C模块

export function greet(name) {
  say(`Hello, ${name}`);
}
1
2
3
4
5

a.js:顶层模块,依赖 B

import { greet } from './b.js';   // 导入B模块

greet('For技术栈'); // 最终输出:C says: Hello, For技术栈
1
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>
1
2
3
4
5
6
7
8
9
10