在早期,JavaScript 一般是用来当做独立执行的脚本,它们的程序比较小,随着 JavaScript 的发展,JavaScript 的程序也更加的复杂,所以就需要将程序按模块划分,并且模块之间可以互相导入,由此催生了很多的规范,如 CMD 与 AMD,现如今 ES6 也提供原生的模块机制支持。

模块真正的魔力所在是仅导出和导入你需要的绑定,而不是将所用东西都放到一个文件。只有很好地理解了导出和导入才能理解模块与脚本的区别。

模块的导出

通过 export 关键字可以将一个值、函数、对象导出,如

export const a = 1;

export function add(x, y) {
return x + y;
}

export const obj = {
name: "Alice",
age: 18
};

export class Person {
constructor(name) {
this.name = name;
}
}

模块不支持动态的导出,不能在函数或者 if 语句块内导出

if (condition) {
// 这样的语法是不允许的
export ...
}

我们导出变量时,可以通过 as 为变量重命名,例如

function add() {}
// 将 add 重命名为 sum,后续导入时直接导入 sum
export add as sum

模块的导入

通过 import 语法可以在其他模块导出的内容进行导入,例如 a.js 导出如下

// a.js
export let name = "Alice";

我们在 b.js 中使用 import 语句进行导入

// b.js
import {name} from "./a.js";

console.log(name); // Alice

需要注意的是,import 导入为只读导入,即不能修改导入的值

import {name} from "./a.js";

// 下面的行为将报错,Uncaught TypeError: Assignment to constant variable.
name = "Bob";

但是考虑下面的情况

// a.js
export let name = "Alice";
export function setName(value) {
name = value;
}
// b.js
import {name, setName} from "./a.js";

// 可以通过 setName 修改 name
setName("Bob");
console.log(name); // Bob

可以通过 setName 来修改 name 的值。 同样,我们也可以通过 as 语法为导入的变量重命名

// b.js
import {name as alice} from "./a.js";

console.log(alice); // Alice

通过 **import *** 可以导入模块中的所有内容

import * as a from './a.js'

console.log(a.name); // Alice

模块的默认值

我们可以通过 export default 导出模块的默认值

// a.js
function add(x, y) {
return x + y;
}

export default add

通过 export default 导出的变量可以直接通过 import 引用

// b.js
import add from './a.js'

默认导出还可以写为

export add as default

这种写法同 export default 的效果一致,另外可以通下面的这种方式为默认导入重命名

// 为默认导入重命名
import {default as add} from './a.js'

当同时导入默认导出与其他导出时,可以这么写

// a.js

// 一般导出
export let name = "Alice";

// 默认导出
export default function add() {
return x + y;
}
// b.js
import add, {name} from './a.js';

浏览器中使用模块

在 script 中使用模块

要在 script 标签中使用模块,需要设置 script 标签的 type=”module”

<script type="module">
import add from './a.js';
</script>

<script type="module" src="./b.js"></script>

模块加载顺序

如果 script 的 type 属性被设置为了 module,那么会自动为每一个 script 标签添加一个 defer 属性,当解析到此 script 标签时,它会立即下载该模块文件,但是下载完成后不会执行,只有等到文档解析完毕才会执行。

并且执行的顺序与 script 定义的顺序有关,定义在前的 script 标签先执行

<script type="module" src="./module1.js"></script>

<script type="module" src="./module2.js"></script>

虽然可能 module2.js 先下载完毕,但是 module1.js 定义在签名,所以 module1.js 会先于 module2.js 执行。

一个模块会被多个模块所引用,但是所有的模块只会被执行一次,如果有多个模块导入同一模块,第一次导入时,执行该模块,然后放入缓存,第二个及以后导入该模块时直接从缓存中获得导入的模块,而不会再次执行此模块。

浏览器中的模块说明符

当我们使用 import 导入模块时,浏览器要求路径是以下几种格式:

  • . /:从当前目录开始解析
  • ../:从父目录开始解析
  • /:从根目录开始解析

所以下面的导入语句在浏览器中是无效的

import add from 'example.js'
import sum from 'example/index.js'

因为上述的导入路径不是以 ./ ../ 或者 / 开头,所以无法被浏览器加载。