Javascript 模块
Zhongjun Qiu 元婴开发者

JavaScript 模块机制详解,涵盖模块定义与作用域特性、export 与 import 的多种用法、默认导出与命名导出、模块在浏览器环境中的加载行为、以及动态导入 import() 表达式的用法与应用场景。

模块概念

一个模块(module)就是一个文件。一个脚本就是一个模块。就这么简单。

模块可以相互加载,并可以使用特殊的指令 exportimport 来交换功能,从另一个模块调用一个模块的函数:

  • export 关键字标记了可以从当前模块外部访问的变量和函数。
  • import 关键字允许从其他模块导入功能。

模块特点

  • 始终使用 “use strict”
  • 模块级作用域

每个模块都有自己的顶级作用域(top-level scope)。换句话说,一个模块中的顶级作用域变量和函数在其他脚本中是不可见的。

只能通过export和import来实现。

  • 模块代码仅在第一次导入时被解析

  • 模块代码仅在第一次导入时被解析

  • import.meta

    包含关于当前模块的信息。

    它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL。

浏览器特定功能

  • 模块脚本是延迟的

模块脚本 总是 被延迟的

  • 下载外部模块脚本 <script type="module" src="..."> 不会阻塞 HTML 的处理,它们会与其他资源并行加载。
  • 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比 HTML 加载速度更快),然后才会运行。
  • 保持脚本的相对顺序:在文档中排在前面的脚本先执行。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="module">
alert(typeof button); // object:脚本可以“看见”下面的 button
// 因为模块是被延迟的(deferred,所以模块脚本会在整个页面加载完成后才运行
</script>

相较于下面这个常规脚本:

<script>
alert(typeof button); // button 为 undefined,脚本看不到下面的元素
// 常规脚本会立即运行,常规脚本的运行是在在处理页面的其余部分之前进行的
</script>

<button id="button">Button</button>

导出和导入

在声明前导出

1
2
3
4
5
6
7
8
9
10
11
12
// 导出数组
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// 导出 const 声明的变量
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// 导出类
export class User {
constructor(name) {
this.name = name;
}
}

导出 class/function 后没有分号

导出与声明分开

1
2
3
4
5
6
7
8
9
10
// 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}

function sayBye(user) {
alert(`Bye, ${user}!`);
}

export {sayHi, sayBye}; // 导出变量列表

也可以把 export 放在函数上面。

Import*

通常,我们把要导入的东西列在花括号 import {...} 中,就像这样:

1
2
3
4
5
// 📁 main.js
import {sayHi, sayBye} from './say.js';

sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!

但是如果有很多要导入的内容,我们可以使用 import * as <obj> 将所有内容导入为一个对象,例如:

1
2
3
4
5
// 📁 main.js
import * as say from './say.js';

say.sayHi('John');
say.sayBye('John');

import和export都可以使用as来重命名

Export default

在实际中,主要有两种模块。

  • 包含库或函数包的模块,像上面的 say.js
  • 声明单个实体的模块,例如模块 user.js 仅导出 class User

export default 放在要导出的实体前:

1
2
3
4
5
6
7
8
9
10
11
// 📁 user.js
export default class User { // 只需要添加 "default" 即可
constructor(name) {
this.name = name;
}
}

// 📁 main.js
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可

new User('John');

每个文件应该只有一个 export default

命名的导出 默认的导出
export class User {...} export default class User {...}
import {User} from ... import User from ...

动态导入

import() 表达式

import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。

1
2
3
4
5
6
7
8
9
10
<!doctype html>
<script>
async function load() {
let say = await import('./say.js');
say.hi(); // Hello!
say.bye(); // Bye!
say.default(); // Module loaded (export default)!
}
</script>
<button onclick="load()">Click me</button>

动态导入在常规脚本中工作时,它们不需要 script type="module".

import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号

 REWARD AUTHOR
 Comments
Comment plugin failed to load
Loading comment plugin