1、什么是模块
一个脚本就是一个模块,一个模块就是一个文件,模块可以相互加载,并可以使用特殊指令export
和import
来交换功能,从另一个模块调用一个模块的函数。
export
标记了外部可以从当前模块访问的变量和函数import
允许从其他模块导入功能
2、模块核心功能
- 始终默认使用
use strict
- 模块级作用域,每个模块都有自己的顶级作用域
- 模块代码仅在第一次导入时被解析,模块代码只执行一次,导出仅创建一次,然后会在导入之间共享
import.meta
对象包括关于当前模块的信息- 在一个模块中,“this”是undefined
- 模块脚本是延迟的
(1)下载外部模块脚本不会阻塞HTML处理,会和其他资源并行加载
(2)模块脚本回等到HTML文档完全准备就绪才会运行
(3)排在前面的脚本先执行
<script type="module">
alert(typeof button); // object:脚本可以“看见”下面的 button
// 因为模块是被延迟的(deferred,所以模块脚本会在整个页面加载完成后才运行
</script>
相较于下面这个常规脚本:
<script>
alert(typeof button); // button 为 undefined,脚本看不到下面的元素
// 常规脚本会立即运行,常规脚本的运行是在在处理页面的其余部分之前进行的
</script>
<button id="button">Button</button>
- Async适用于内联脚本
<!--
外联的写法:
注意:外联的脚本块中不要再写代码,
尽量不要用空标签简写方法。
-->
<!--<script language="JavaScript" src="../js/js1.js"></script>-->
<!--内联的写法1(不推荐)-->
<!--<script>
alert("Hello Wolrd!");//显示消息框
//alert有类似于断点的作用,
//所以之前调试代码是用的它。
</script>-->
<!--内联的写法2-->
<!--<script launguage="JavaScript">
console.log("Hello Wolrd!");
</script>-->
<!--内联的写法3-->
<!--<script type="application/javascript"></script>-->
<!--内联的写法4-->
<script type="text/javascript"></script>
<!-- 所有依赖都获取完成(analytics.js)然后脚本开始运行 -->
<!-- 不会等待 HTML 文档或者其他 <script> 标签 -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
- 不允许裸模块,import必须给出相对或者绝对路径
- 兼容性 nomodule`
3、导出和导入
3.1、在声明前导出
// 导出数组
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 后没有分号
注意,在类或者函数前的 export 不会让它们变成函数表达式。尽管被导出了,但它仍然是一个函数声明。
export function sayHi(user) {
alert(`Hello, ${user}!`);
} // 在这里没有分号 ;
3.2、导出与声明分开
先声明函数,然后再导出它们
/ 📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export {sayHi, sayBye}; // 导出变量列表
3.3、Import *
把要导入的东西列在花括号 import {…} 中
// 📁 main.js
import {sayHi, sayBye} from './say.js';
sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!
果有很多要导入的内容,我们可以使用 import * as 将所有内容导入为一个对象
// 📁 main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
为什么通常要明确列出我们需要导入的内容
- 现代的构建工具(webpack 和其他工具)将模块打包到一起并对其进行优化,以加快加载速度并删除未使用的代码,我们向我们的项目里添加一个第三方库 say.js,它具有许多函数而我们只在我们的项目里使用了 say.js 中的一个函数,优化器(optimizer)就会检测到它,并从打包好的代码中删除那些未被使用的函数,从而使构建更小。这就是所谓的“摇树(tree-shaking)”
- 明确列出要导入的内容会使得名称较短:sayHi() 而不是 say.sayHi()
- 导入的显式列表可以更好地概述代码结构:使用的内容和位置。它使得代码支持重构,并且重构起来更容易
3.4、Import “as”
用 as 让导入具有不同的名字
// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';
hi('John'); // Hello, John!
bye('John'); // Bye, John!
3.5、Export “as”
// 📁 say.js
...
export {sayHi as hi, sayBye as bye};
// 📁 main.js
import * as say from './say.js';
say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!
3.6、Export default
在实际中,主要有两种模块。
- 包含库或函数包的模块
- 声明单个实体的模块
模块提供了一个特殊的默认导出 export default 语法,以使“一个模块只做一件事”的方式看起来更好
// 📁 user.js
export default class User { // 只需要添加 "default" 即可
constructor(name) {
this.name = name;
}
}
然后将其导入而不需要花括号
// 📁 main.js
import User from './user.js'; // 不需要花括号 {User},只需要写成 User 即可
new User('John');
import
命名的导出时需要花括号,而 import
默认的导出时不需要花括号。
命名的导出 | 默认的导出 |
---|---|
export class User {…} | export default class User {…} |
import {User} from … | import User from … |
命名的导出会强制我们使用正确的名称进行导入:
import {User} from './user.js';
// 导入 {MyUser} 不起作用,导入名字必须为 {User}
对于默认的导出,我们总是在导入时选择名称
```javascript
import User from './user.js'; // 有效
import MyUser from './user.js'; // 也有效
// 使用任何名称导入都没有问题
3.7、重新导出
语法 export ... from ...
export {sayHi} from './say.js'; // 重新导出 sayHi
export {default as User} from './user.js'; // 重新导出 default
- export {x [as y], …} from “module”
- export * from “module”(不会重新导出默认的导出)。
- export {default [as y]} from “module”(重新导出默认的导出)。
在 {…} 中的 import/export 语句无效。
if (something) {
import {sayHi} from "./say.js"; // Error: import must be at top level
}
4、动态导入
上面的导出和导入语句称为“静态”导入。语法非常简单且严格。
- 模块路径必须是原始类型字符串,不能是函数调用
import ... from getModuleName(); // Error, only from "string" is allowed
- 无法根据条件或者在运行时导入
if(...) {
import ...; // Error, not allowed!
}
{
import ...; // Error, we can't put import in any block
}
4.1、import() 表达式
import(module)
表达式加载模块并返回一个 promise
,该 promise resolve
为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。
let modulePath = prompt("Which module to load?");
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
动态导入在常规脚本中工作时,它们不需要 script type="module".
//say.js
export function hi() {
alert(`Hello`);
}
export function bye() {
alert(`Bye`);
}
export default function() {
alert("Module loaded (export default)!");
}
//main.js
<!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>
尽管 import()
看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super()
)。因此,我们不能将 import
复制到一个变量中,或者对其使用 call/apply
。因为它不是一个函数。